> For the complete documentation index, see [llms.txt](https://klassdocs.gitbook.io/klassapp-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://klassdocs.gitbook.io/klassapp-documentation/docs/research/progress-2026-10-03.md).

# Soft-launch Prod-Readiness — Progress (2026-10-02/03)

Autonomous run log for Rasta's Part A/B/C prompt. Live SHA of `main`: **`2a22067c`** (`merged: true` style verified via GitHub API for every PR in this file). Latest staging deploy: succeeded on `2a22067c`.

## Shipped (Part A)

| Item                                                                                                       | PR   | Merge SHA  | Evidence                                                                                                                                                             |
| ---------------------------------------------------------------------------------------------------------- | ---- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A1 #951 rebase + merge                                                                                     | #951 | `b4541b5b` | suite 2127/0/0/4 before merge; CI green; staging succeeded                                                                                                           |
| A1-fix missing `</div>` (preview branch of `agent-toshi.blade.php` killed Livewire update script on morph) | #957 | `16788239` | failing-then-passing `ToshiTemplateMarkupBalanceTest` (preview Δ1→0); staging re-verify: console `errors: []`, warning gone                                          |
| A2 Livewire 3.8.2→3.8.10                                                                                   | #958 | `e7731bdd` | single-package lock bump; suite 2133/0/0/4; CI green                                                                                                                 |
| A2 follow-up republish `public/vendor/livewire`                                                            | #959 | `72b47b48` | kills "published Livewire assets are out of date" on every page; suite 2133/0/0/4                                                                                    |
| A3 harsh-default backfill for existing schools                                                             | #960 | `79c98811` | deploy log: `[toshi backfill] flipped 26 school(s): assistant=26, flag-only=0`; staging DB after: enabled=0 (all 39), assistant=0; down() restores from JSON capture |

A2 staging page checks (after the staging deploy): dashboard, `/admin/student/add`, `/admin/teacher/add` @1280+375 → HTTP 200, 0 console errors, `data-update-uri` present, preview root intact.

## A4 manual wizard journeys (staging @1280+375, DB-verified)

3 throwaway signup accounts; full 17-step wizard per school. All finished with review cards + "is ready".

| Journey | School (id)                         | Category      | Classes seeded            | Subjects | Terms | Fees                     | WhatsApp | Plan       |
| ------- | ----------------------------------- | ------------- | ------------------------- | -------- | ----- | ------------------------ | -------- | ---------- |
| Primary | Kampala Journey Primary (102)       | primary ✓     | `["primary"]` ✗           | 28 ✓     | 3 ✓   | Development Fund 25000 ✓ | row ✓    | Freemium ✓ |
| O-level | Journey O-Level Academy (103)       | o\_level ✓    | `["o-level"]` ✗           | 28 ✓     | 3 ✓   | Boarding Fee 180000 ✓    | row ✓    | Freemium ✓ |
| O+A     | Journey O and A Level College (104) | o\_a\_level ✓ | `["a-level","o-level"]` ✗ | 50 ✓     | 3 ✓   | Tuition Term 1 ×2 ⚠️     | row ✓    | Freemium ✓ |

### A4 findings (pre-production blockers)

1. **Class seeding wrong** (matches Part B failure): wizard saves tier-bands (`primary`/`o-level`) as class rows, while the step UI *displays* proper-looking names ("Primary Five", "Senior Four", "Senior Five") — display layer and persistence disagree. Should seed P.1–P.7 / S.1–S.6 (+A-level band classes per category).
2. **Skip/finish not sticky**: after reload the wizard dropped back to step 11 (Teachers) on the O+A school that had already been finished (`"is ready"` shown) — `skipOptionalStep` doesn't persist `is_complete`.
3. **pageerror on finish** (repro 2×): `TypeError: Cannot read properties of null (reading 'before')` when the wizard transitions to the ready state.
4. J3 duplicate fee = automation artifact (driver added twice), not attributed to product — flagged, needs one look in Part B.

Screenshots: `a4-j1-…-1280.png`, `a4-j3-ready-375.png`, `a4-j3-reload-1280.png` (fonts hang previously blocked shots; 9s-timer shots worked).

## Ops notes

* Staging mailer is `log` and the runtime-log ingest is failing (`Ingest failed [0s]: No authentication details`) — email OTP codes are effectively unreadable on staging (hashed at rest). Journeys were unblocked by pre-verifying our own signup accounts.
* Phone regex for signup: `07[0578]` (+256/0 optional). Digits are not allowed in the full-name field.
* Valid OTP surfaces on-page for WhatsApp (wizard step shows the code).
* Known GitHub Dependabot count (69 open, 26 high) spans sub-dependencies unrelated to this soft-launch — tracked in Rasta's internal hygiene list, not fixed here.

## Soft-launch Parts 1–3 run (2026-10-03)

Worktree left #961 alone. Cloud Commands via `kc.py` / stg\_bridge OK this session.

### Staging verify — A4 “wrong class names” (schools 102–104)

| School                            | category    | standards (tier)    | sections (classes) |
| --------------------------------- | ----------- | ------------------- | ------------------ |
| 102 Kampala Journey Primary       | primary     | `primary`           | Primary One…Seven  |
| 103 Journey O-Level Academy       | o\_level    | `o-level`           | Senior One…Four    |
| 104 Journey O and A Level College | o\_a\_level | `o-level`,`a-level` | Senior One…Six     |

**Verdict:** A4 table logged `standards.name` as “Classes seeded”. Persistence was already correct. P.1/S.1 rename deferred (keep Primary One / Senior One).

### Part 1a PR A — class/section contract

* PR: [#962](https://github.com/KlassApp-Foundation/KlassApp/pull/962) MERGED `b0b28d26`

### Part 1a PR B — skip/finish sticky

* PR: [#963](https://github.com/KlassApp-Foundation/KlassApp/pull/963) MERGED `039d12d6`

### Part 1a PR C — finish pageerror morph

* PR: [#964](https://github.com/KlassApp-Foundation/KlassApp/pull/964) MERGED `f8a01b9c`

### Part 1b — staging mail OTP sandbox

* Root cause of `Ingest failed: No authentication details`: **Nightwatch** APM (also `403 Exceeded quota`), not Cloud API auth. `NIGHTWATCH_TOKEN` set; `NIGHTWATCH_INGEST_URI` null.
* Fix: when `MAIL_MAILER=log`, emit `klassapp.email_verification_code` structured log for Cloud socket; E2E reads logs first, Commands re-issue fallback; **no pre-verify**.
* **Rasta:** fix Nightwatch quota/token or remove `nightwatch` from staging `LOG_STACK`.
* PR: [#965](https://github.com/KlassApp-Foundation/KlassApp/pull/965) MERGED `ed74201f`

### Part 1c — manual journeys re-run

### Part 1c — manual journeys (staging)

| Journey        | 1280 | 375               | Notes                                                                                                     |
| -------------- | ---- | ----------------- | --------------------------------------------------------------------------------------------------------- |
| primary-manual | PASS | PASS              | 0 failed checks                                                                                           |
| olevel-manual  | PASS | PASS              | 0 failed checks                                                                                           |
| oalevel-manual | PASS | (running/see log) | 0 failed checks; 2 soft findings (A-level combinations / GP classification — product flags, not blockers) |

### Part 3c — wizard vs Toshi v2 DB parity

* PR: [#970](https://github.com/KlassApp-Foundation/KlassApp/pull/970) MERGED `dc38ae2d`
* primary-manual @1280: wizard completed (0 failed hard checks) then Playwright crashed with `ENOSPC` (disk full). Freed \~4GB (artifacts, npm cache, finished worktree vendor/node\_modules). Re-run remaining journeys after 3a ships / disk stays healthy.
* Staging tip may still lag `main` if Cloud GitHub re-auth blocks deploy (POST 422) — journeys hit whatever SHA is live.

### Part 2 — proposal

* PR: [#966](https://github.com/KlassApp-Foundation/KlassApp/pull/966) MERGED `98381264`

### Part 3a — step registry (contract + classes + tests)

* Branch: `feat/onboarding-step-registry`
* `App\Onboarding\Steps\{Contracts,Abstract,StepRegistry,Steps\*}` — 16 steps matching `OnboardingStepsService::ALL_STEPS`
* Choice steps normalize labels→keys (`UNEB`→`uneb`, category labels) inside `save()`
* Contract tests: `tests/Feature/Onboarding/Steps/OnboardingStepContractTest.php` — 8 passed locally
* PR: [#967](https://github.com/KlassApp-Foundation/KlassApp/pull/967) MERGED `dcd4c29c`
* primary-manual @1280: **PASS** (0 failed checks) after disk free
* primary-manual @375: **PASS** (0 failed checks)

### Part 3b — Toshi onboarding\_v2

* PR: [#968](https://github.com/KlassApp-Foundation/KlassApp/pull/968) MERGED `53c05251aaf6a2c535f34ee0a66b7f1d67e69d6f`
* `config('toshi.onboarding_v2')` / `TOSHI_ONBOARDING_V2` default false
* `ToshiOnboardingV2Driver` + AgentToshi `onboarding_v2` actionStep
* Tests: `ToshiOnboardingV2DriverTest` (3 passed; CI test-guard green)
* 1c: primary-manual @1280/@375 PASS; olevel-manual @1280 PASS

### Part 3c — wizard vs Toshi v2 DB parity

* PR: [#970](https://github.com/KlassApp-Foundation/KlassApp/pull/970) MERGED `dc38ae2d`

### Part 3d — Toshi v2 on staging (BLOCKED)

* Cloud env var set: `TOSHI_ONBOARDING_V2=true` on staging (API `method=set`) — **ready for next deploy** (visible in Cloud env var list; **not** injected into runtime until a real deploy)
* Staging still on `79c98811` (pre-registry). Last failed deploy at `b0b28d26`
* **Re-verify 2026-10-03 \~15:50Z UTC:** empty-body deploy POST again → **422** `source control provider requires re-authorization`; staging HTTP **200** (awake); Commands probe: `DRIVER_NO` / `REG_NO` / `config('toshi.onboarding_v2')=null`
* Commands-API filesystem overlay does **not** stick to the web fleet (ephemeral command containers) — cannot hotspot v2 code without a real deploy
* primary-toshi @1280 attempt: HTTP **503** during signup (hibernation / old tip); not a product failure
* stg\_bridge `command.created` fix: [#972](https://github.com/KlassApp-Foundation/KlassApp/pull/972) MERGED `badfc522`
* **Rasta:** re-authorize GitHub on the Laravel Cloud app → empty-body deploy staging → verify `config('toshi.onboarding_v2')===true` → rerun `primary-toshi` / `olevel-toshi` / `oalevel-toshi` @1280+375
* 3e deferred until 3d green

### Part 3d re-verify (2026-10-04) — first re-verify attempt still BLOCKED

* Sync: `origin/main` = `60c8c900` (#973 merged). Open-PR scan: 11 open — #961/#937/#936/#935/#822/#821/#786/#732/#728 (own drafts) + #890/#682 (external drafts, Elicom256). None touched.
* Staging deployments via API: last success at `79c98811`; last failed at `b0b28d26` — unchanged from 2026-10-03.
* Deploy POST (empty body) ×2 on 2026-10-04: **HTTP 422** — `{"message":"The associated source control provider requires re-authorization."}` — the reported re-auth is not yet effective on the Cloud app.
* Healthy meanwhile: staging HTTP 200; Commands API works (`php artisan --version` → Laravel 12.63.0); staging env var `TOSHI_ONBOARDING_V2=true` still set from 2026-10-03 (dormant until a real deploy).
* Doppler CLI on this machine returned `Invalid Auth token` (unauthenticated); Cloud token sourced via the documented fallback `~/.cursor/mcp.json` → `laravel-cloud` → `X-Auth-Token` (same org token; never printed).
* Session 0/1 verified this session: GitHub (admin, Mucunguzi256), Cloud token ✓, Commands bridge ✓, staging HTTP ✓, PHP 8.4.19/Node local ✓. Steps 2/3 correctly not attempted per instructions.

## Next

(Superseded 2026-10-05: re-auth done, staging deploys working — tip `d34863c8`; 3d journeys 12/12 PASS, see "Journey rerun" below; 3e shipped as #979. Original next-steps preserved in the Part 3d entries above.)

### Part 3d — resumed 2026-10-04 (Step 1 + OTP source check)

* **Done:** staging re-verified read-only via Cloud API: latest deploy @ `60c8c900` `deployment.succeeded`; Commands bridge: `config('toshi.onboarding_v2')=true`, `ToshiOnboardingV2Driver` present, default mode `preview`.
* **Done:** harness branch `e2e/toshi-v2-harness` (base `60c8c900`): `enableOnboardingMode` defined + exported from `lib/outcomes.js`; static import/export cross-check across `e2e/onboarding` = 0 mismatches; `node --check` OK on all harness JS; `playwright test --list` lists 12 tests; v2 `decide()` wordings match all 16 `question()` strings in `app/Onboarding/Steps/Steps/`.
* **Done (harness fix):** `lib/stg_logs.py` only searched the log row `message`; Cloud puts the `Log::info` context (email + code) under `data.context`, so the OTP was never found and `signupValid` would silently fall back to the Commands re-issue. Now matches message + structured payload. Verified with a throwaway E2E sign-up: code read from the Cloud log sandbox (`source=logs`), email verified in-UI, no pre-verify, no re-issue.
* **Done:** guarded `enableOnboardingMode` exercised live on the throwaway E2E school (`toshi_mode=onboarding`, `toshi_enabled=0`; dashboard shows "Set up with Toshi"); refuses non-E2E users (`not-an-e2e-user`).
* **Not available:** the rendered log-mailer message ("Your KlassApp code is …") is not in Cloud logs — the mail is queued and staging has no queue worker; the structured marker is the only log source.
* **In progress:** throwaway schools purge dry-run done (force pending). Leftover school from the 2026-10-03 run is not named `E2E …`, so `test:purge-schools` refuses it (stays flagged inactive).
* **Next:** Step 2 Toshi v2 journeys (primary/olevel/oalevel-toshi @375/@1280).

### Part 3d — Step 2 Toshi v2 journeys (2026-10-04)

* **Harness fixes (branch `e2e/toshi-v2-harness`):** with AI off, finishing the last v2 step calls `exitCompletingSetupMode()` → `enterSetupDoneMode()`, which clears the chat and shows the Coming soon card, so the transcript regexes never saw a completion line (first primary-toshi @1280: 13 turns, 0 failed DB checks, "no completion signal"). The driver now counts that card as done once the scripted chat has run, and Toshi runs gain a hard check that `StepRegistry::nextUnfinished()` is null. Journeys also save final panel / dashboard / classes-page screenshots.
* **Result: 6/6 PASS** (full set, one run, hard checks, codes read from the Cloud log sandbox on every sign-up, no re-issue fallback): primary-toshi, olevel-toshi, oalevel-toshi at 1280 and 375 — 13 turns each; terms (3), WhatsApp linked, plan, the type's exact class names, registry complete. Individual debug runs before the full set also passed.
* **Note:** the v2 Toshi path has no review screen (that is the manual wizard's); class names are verified in the DB. The Classes page shows no class list until teachers exist ("add teachers before assigning"), so it is not usable as visible evidence.
* **Preview default check: PASS** at 1280 and 375 — Coming soon card + "Set up manually", no "Set up with Toshi".
* **Blocked (app):** `test:purge-schools --force` fails on the first school with FK `authentications_user_id_foreign` (email-verification rows from sign-up are not in the command's user-scoped tables). Transaction rolled back; nothing deleted. Dry run is fine. Fix + failing test proposed; awaiting go-ahead because a merge redeploys staging. E2E schools from this run stay in place until then.
* **Next:** purge command fix → force purge → Step 3 only after go-ahead.

(Last Next line — completed 2026-10-05: re-auth done, staging deployed, 3d journeys passed, 3e shipped as #979.)

## 2026-10-05 (session: verified-email gate / purge FK / harness)

### Part 4 — Require verified email at sign-in

* Branch `fix/require-verified-email-signin` (9fbcd45b + a3e49667) — local guarded full suite rerun #2: **2167 tests, 0 failures, 0 errors** (4 pre-existing skips)
* PR: [#975](https://github.com/KlassApp-Foundation/KlassApp/pull/975) OPEN (title: Require verified email at sign-in), CI attempt 1: `test-guard` FAIL with 4 flaky-style failures (`IsResetEnforcementTest` ×3, `TeacherLoginRedirectTest` ×1) — same tests pass locally in isolation AND in two fresh full-suite runs (2167/0), no repro; failed-job rerun armed
* Staging probe (pre-deploy): `unverified_with_email=24`, all created pre-gate-cut date → **0 accounts affected** by the new rule (cutoff-equivalent backfill is in `EmailVerificationGate::needsVerification`)
* `KlassApp-Foundation/internal` reachable → detailed note to go there post-merge

### Part 5 — purge command FK fix

* Staging FK audit 2026-10-05: user-FK tables with rows for target schools 106-111,113,116 = authentications 32 (NO ACTION → fails purge), school\_grading\_systems 86 (cascade-covered); users.ref\_id unused (0/0)
* PR: [#976](https://github.com/KlassApp-Foundation/KlassApp/pull/976) OPEN — `authentications` + onboarding/WhatsApp/parent-profile/Toshi persona rows deleted by `user_id` before `users`; student\_parent\_links + co\_admin\_invites join the school pass; FK-enforcing regression test (fails-first: `FOREIGN KEY constraint failed … delete from "users"`) — 7/7 green locally
* School 114 "Suite's School-25": **already status=0** (inactive) — left as-is, reported
* School 115 "Suite's School-26" (same 2026-10-04 e2e run, still status=1): flagged **inactive (status=0)** via staging, verified; toshi\_enabled stays 0
* Purge force still pending: after #976 merge + deploy (dry run first, then force over 106-111,113,116 + 117-132 leftovers)

### Part 6 — e2e harness branch

* `e2e/toshi-v2-harness` pushed (58fb2e8c atop 5dfe630b): stg\_logs code-extraction now email-bound; PR: [#977](https://github.com/KlassApp-Foundation/KlassApp/pull/977) OPEN (e2e-only)
* \#977 test-guard red: same 4-login-test failure class as below (fixture pin silently dropped), not a harness defect

### CI red again, 2026-10-05 later: the "pinned" fixtures were never pinned

* All four login tests (IsResetEnforcement ×3, TeacherLoginRedirect ×1) went red on CI again after the wall clock crossed the gate cutoff instant (2026-10-05 00:00 UTC). Root cause this time is a fixture bug, not the gate: `'created_at' => '2026-09-15 09:00:00'` was passed **inside `User::create([...])`**, and `created_at` is not in `User::$fillable` → Laravel silently drops it, rows get `created_at = now()`, and the "pinned" tests were green only while every CI run still happened **before** the cutoff instant (#975 @23:22 UTC, #976 @23:38 UTC Oct 4). Same silent non-pin is on `main` — every future CI run fails it.
* Fix: PR [#978](https://github.com/KlassApp-Foundation/KlassApp/pull/978) `fix/pin-gate-fixture-created-at` (test-files-only): direct `DB::table('users')->update(['created_at' => …])` + `refresh()` via a `pinCreatedAt()` helper, with an `assertSame` on the **raw DB value** so a dropped pin fails as a fixture error, timezone-independently. Failing-first on clean main (4 red, all → `/register/verify`); after: 10/10 under both Kampala and UTC boots, 14/14 gate tests under both boots. Full guarded suite on the PR head under the Kampala boot: 2163 passed, 0 failures, 2 risky, 4 skipped. CI test-guard/check/scan green → **merged `abf977a5`**, staging redeploy triggered (test-only, no runtime behavior).
* \#977 rebased onto `abf977a5` (6 commits clean on top of fixed fixtures); CI green (test-guard/check/scan/GitBook) → **merged `04650071`** ("E2E: toshi v2 harness and stricter verification-code matching"). Staging redeploy triggered (harness/e2e-only; no app runtime code on the branch).
* Internal postmortem amended with a correction section (KlassApp-Foundation/internal `main` @ `ec5659f`/`40e9693`).
* **Tasks 1–4 of the current brief complete.** Next: Task 5 (manual wizard rendered from the step registry + preview/idempotence for the 16 registry steps, per-step (b)+(d) tests, journeys after deploy), then Task 6 (#974 stamp — needs Rasta's explicit go-ahead before merge).

## Next

\#978 test-guard green → merge → rebase #977 onto new main → CI green → merge → staging deploy(s) watched; then remaining brief items.

### CI red on #975 root-caused: the gate's cutoff read the app timezone

* CI test-guard failed #975 twice (4 tests: IsResetEnforcement x3 POST /login + TeacherLoginRedirect x1) while the same branch passed the full suite locally twice. Reproduced locally in one command: `TIMEZONE=Africa/Kampala php artisan test tests/Feature/Auth/TeacherLoginRedirectTest.php tests/Feature/Auth/IsResetEnforcementTest.php` -> the same 4 fail, all redirected to `register/verify`. Trigger: .env.example sets TIMEZONE=Africa/Kampala (CI's env), and at CI run time the wall clock was already past Kampala midnight of the cutoff (2026-10-05), while the app-timezone startOfDay comparison in EmailVerificationGate still uses no fixed boundary.
* Gate fix (verified-email branch): the cutoff is now a fixed instant - `Carbon::parse(self::CUTOFF, 'UTC')` compared directly against created\_at, no startOfDay. Exempt iff created\_at < 2026-10-05 00:00:00 UTC regardless of deployment timezone. Widens the exempt set vs the Kampala-lens math only in the 21:00-00:00 UTC window (permissive direction; no new lockouts).
* New failing-first test: VerifiedEmailAtLoginTest::test\_cutoff\_boundary\_is\_a\_utc\_instant\_not\_an\_app\_timezone\_day (barely-detachable Kampala-lens scenario via runtime date\_default\_timezone\_set; self-restoring). Fails at the boundary assert on the old code, passes on the new one under both boot lenses.
* The four stale login tests pinned: created\_at 2026-09-15 09:00:00 + email\_verified 0 on the three IsReset /login users and the TeacherLoginRedirect teacher. Their tested contract is "pre-gate accounts keep signing in", which needs a pinned era now that the cutoff date is live; nothing asserted is weakened.
* Evidence: new boundary test failing at line 360 with the old gate (git stash round-trip); 4 CI-equivalent failures local; after fix 14/14 gate tests under both boots, 10/10 login tests under both boots, and a full suite under TIMEZONE=Africa/Kampala: 2162 passed, 0 failures, 2 risky, 4 skipped.

## 2026-10-05 (continued): merges, deploy, staging verification, purge

### Merges (admin squash; ruleset needs a review, brief authorises)

* [#975](https://github.com/KlassApp-Foundation/KlassApp/pull/975) merged (squash `823e501b`) after CI green; #976 rebased onto post-#975 main so PR CI built the combined tree; [#976](https://github.com/KlassApp-Foundation/KlassApp/pull/976) merged (squash `58338260`). Merged between test runs; staging deploys watched to completion by polling the bridge until its 422-deploy-in-progress stops — no merges during an active deploy.

### Staging lens (empirical, no env values read)

* Bridge NOW 02:27 UTC-7-logged vs `date -u` 23:28 → staging wall clock = UTC+3 (Kampala). Gate cutoff 2026-10-05 00:00 UTC == 03:00 Kampala; gate-probe fixture created at 00:05 UTC, i.e. past cutoff in every lens.

### Task 1 live verification (staging, post-deploy; screenshots in \~/litellm)

* Pre-cutoff unverified (user 606, created 21:16 Oct 4 UTC): login → `/admin/dashboard`, no lockout, @375 and @1280. **0 staging accounts affected by the gate** (24 unverified, all pre-cutoff; the only one exercised live was 606 providing behavioural proof).
* Post-cutoff unverified (user 623, E2E Gate Verification school): login → `/register/verify` code-entry screen @375 and @1280; resend button issues a fresh 6-digit code (found in app log, email-bound extraction); entering the code confirms → `/admin/dashboard`; second login for the now-verified account → `/admin/dashboard` directly, no verification screen.
* PR [#977](https://github.com/KlassApp-Foundation/KlassApp/pull/977) (e2e-only harness hardening) merged after all journeys ran: code fallback now only accepts a code when the account's email matches.
* Private postmortem pushed to `KlassApp-Foundation/internal` `main` as `03162d38` — `docs/klassapp/2026-10-05-email-gate-ci-timezone-postmortem.md` (timeline, root cause, gate fix, verification numbers, live staging results; PR #13 there, squash-merged). Public repo never carried the timezone-root-cause detail.

### Task 2 purge on staging (post FK fix, command live)

* Dry run over `[106-111,113,116-132,134]` (25 schools): all 25 present, `is_test=1 is_demo=0`, all names `E2E …`; protected-name check found no Demo Junior/Senior/Academy, no Phase 4 Roster Test Fixture in the set; `authentications` rows now inventoried per school (4 per manual school, 2 per Toshi school); exit 0, nothing deleted.
* Force run: all 25 purged, exit 0. Post-state: `schools remaining=0 users remaining=0` over the set; is\_test schools left 18 (first four are the protected ones: Phase 4 Roster Test Fixture, Demo Academy Uganda, Demo Junior School, Demo Senior School, all status 1 = untouched); user range 560-623 has only 4 survivors — uid 596 (Suite's School-24), 603 (Suite's School-25=114), 604 (Suite's School-26=115), 622 (Boris's School 133, test=0, real user), all in schools deliberately outside the purge set; gate fixture 623 and fanout 606 gone; schools 112/133 untouched (test=0). Schools 114/115 stay flagged inactive per rule 8/9.

### Files

* Purge scripts: /tmp/purge-dryrun.php, /tmp/purge-force.php, /tmp/purge-verify\*.php (via stg\_bridge).

## 2026-10-05 (continued): Task 5 — wizard from the step registry (PR #979)

Branch `feat/wizard-from-step-registry` off `04650071` (post-#977 main), worktree KlassApp-wt-gatepins, commit `0584baac`, PR [#979](https://github.com/KlassApp-Foundation/KlassApp/pull/979).

### Failing-first evidence

* New `tests/Feature/Onboarding/Steps/StepIdempotenceTest.php` first run on the unimplemented branch: all 17 tests errored `Call to undefined method ...::saveAndReport()` at the `reportOf/prepareThrough` helper lines — the new-contract requirements existed nowhere in code.

### What the PR changes

* `OnboardingStep` contract gains `preview()` (read-only `{action: noop|skip|change, summary, rows[]}` — `InvalidArgumentException` propagates from `validate()`) and `saveAndReport()` (`{created[], skipped[]: label+reason}`); `save()` stays void, so the Toshi v2 driver path is untouched except for the (d) fixes.
* All 16 registry steps implement both. Scalar steps (school\_name, student\_size, country, curriculum, school\_category, emis, uneb\_center) share `attributeSaveAndReport`/`attributePreview`; standards/subjects/terms/fees watch row counts; teachers/students add a step-level dedupe pre-pass (identity: email / school\_student\_id / LIN in this school, else name+class when no identifiers) so a rerun filters already-present rows before the engine and reports them skipped — the cross-school global-unique refusals (engine's loud emails/LIN asserts) are deliberately preserved; whatsapp surfaces the engine's existing-link skip with the submitted phone as the label; plan no-ops when already on the same plan.
* `ManualOnboardingWizard::refreshSteps()` now builds order/applicability from `StepRegistry::forSchool()` and pulls label/icon/is\_complete/route from `OnboardingStepsService::steps()` rows (byte-identical rows to before, so checklist markup didn't move), with a `RuntimeException` guard if registry and service keys drift.
* No Drive code; every step accepts structured answers with no chat/Livewire/session dependency.

### Test evidence

* StepIdempotenceTest: 17/17 (156 assertions) — per-step structured save, same-answer rerun with exact skipped labels/counts unchanged, and one preview-never-writes sweep over all 16.
* Full `tests/Feature/Onboarding` tree: 448 passed (2142 assertions), includes all manual-wizard, Toshi v2 parity, and wizard-regression suites.
* Full guarded suite under `TIMEZONE=Africa/Kampala` boot (the CI lens): **2180 passed, 0 failures**, 2 risky, 4 skipped (pre-existing non-blockers), 13116 assertions, 319 s.

CI on #979 (head `0584baac`): checks + scan + test-guard all SUCCESS. Merged via admin squash at 2026-10-05T02:00:05Z as `d34863c8`. Staging auto-deploy verified with the bridge probe: `DEPLOY-CHECK contract-ready 2026-10-05 05:04:29` (`method_exists(OnboardingStep::class, 'saveAndReport')` true on staging).

### Journey rerun (Task 5 gate) — 12/12 PASS

* Run 1 (from `/Users/mac/projects/KlassApp`, working copy at stale `d97356ff`): manual 6/6 ✓; Toshi 6/6 ✘ (`turns=0` — old harness predates the #977 preview-mode flip + v2 prompt rules; staging soft-launch default `TOSHI_DEFAULT_MODE=preview` shows the Coming-soon card). Operator-side stale checkout, **not** a product regression — #979 touched no Toshi runtime code (git diff `04650071..d34863c8` on `app/Livewire/AgentToshi.php`, `app/Onboarding/Steps/ToshiOnboardingV2Driver.php`: empty).
* Run 2 (from worktree at merge commit `d34863c8`): **12 passed / 0 failed / 0 skipped, exit 0, 22.6m.** Server-side v2 chain separately verified `status=done` end-to-end over a throwaway `is_test` fixture via the staging bridge.
* Journey table (run 2, hard checks only):

| Journey        | 1280   | 375    | notes                                                                                        |
| -------------- | ------ | ------ | -------------------------------------------------------------------------------------------- |
| primary-manual | ✓ 1.6m | ✓ 1.6m | 0 failed checks, 0 findings                                                                  |
| olevel-manual  | ✓ 1.6m | ✓ 1.6m | desktop 2 findings (pre-existing: streams-not-scripted/UI notes); mobile 0                   |
| oalevel-manual | ✓ 1.6m | ✓ 1.6m | 2 findings each (same category: A-level combos "flagged for review")                         |
| primary-toshi  | ✓ 1.9m | ✓ 2.1m | turns=13, failed checks=0                                                                    |
| olevel-toshi   | ✓ 1.9m | ✓ 2.1m | turns=13, failed checks=0                                                                    |
| oalevel-toshi  | ✓ 2.0m | ✓ 1.9m | turns=13, failed checks=0; 4 findings (classes-page display + streams + A-level combo notes) |

* All findings are the harness's pre-existing informational notes (streams intentionally not scripted in Toshi; A-level combination/principal-subject modelling "flagged for review") — zero hard-check failures anywhere.
* Revert fallback not needed: nothing failed on the wizard change itself.

### Fixture purge after journeys (Task-2 command, staging, bridge-invoked)

* Renamed the Toshi run's `Suite's School-27…32` fixtures to `E2E Toshi Fixture …` and the idle `Suite's School-16…18/20…22/25/26` leftovers to `E2E Leftover Fixture …` so the sanctioned purge command accepts them (its guard only takes `E2E …` names).
* Dry run over 34 schools `[72,73,74,81,82,83,114,115,135–160]`: all 34 planned, 0 refusals, protected names absent (school 1 not in set). Then `--force`: **34 schools purged** (94 users: 34 admins, 24 teachers, 36 students + dependent rows each).
* Post-purge `is_test` inventory: 7 remain and stay — 1 `Phase 4 Roster Test Fixture` (protected), 3 demo academies, 3 already-inactive (`status=0`) early fixtures with non-E2E names. 12 active schools on staging overall.

## 2026-10-05 (later): brief continuation — #974 + #981 landed; Task 1 live check done

* **#974 merged** `b3e6cc9b` (12:56Z) and **#981 merged** `6fb10eeb` (13:27Z, `EMAIL_VERIFICATION_CUTOFF`), CI green on both heads; staging deploy succeeded on `6fb10eeb` = main tip.
* **Task 1's remaining live check done on staging**: bridge probe showed `config('app.email_verification_cutoff')` NULL (env unset → built-in default) and `cutoffInstant()` present. Created fixture school 161 `E2E Cutoff Check School` (is\_test) with two unverified admins created\_at pinned by direct DB write (the #978 lesson): uid 710 @ 2026-10-04 22:00 UTC (pre-cutoff), uid 711 @ 2026-10-05 13:00 UTC (post-cutoff). Real-browser logins: 710 → `/admin/dashboard` (signs in), 711 → `/register/verify` code screen. Fixture purged with `test:purge-schools --school=161` (dry run then `--force`; post-state school=0 users=0).

## 2026-10-05: Task 2 — production readiness (read-only) + deploy checklist PR

* **Staging (2a)**: latest deploy = `deployment.succeeded` on `6fb10eeb` — staging serves main's tip.
* **Migrations (2b)**: production's latest deploy succeeded on `dabfb80c`; read-only `migrate:status` on production: **0 pending**. `dabfb80c..main` adds 11 migrations — 8 structure-only, 2 data (student\_size bucket re-map; the existing-schools Toshi reset `2026_10_02_183000_backfill_existing_schools_toshi_safe_defaults` which flips assistant→preview + `toshi_enabled=0` and writes a restore log), plus the toshi\_mode add/backfill pair also touching data. Nothing destructive; **no gate backfill migration exists** (the gate is code-only). Modified `2026_09_22_010000_add_is_demo_…` (seeding stripped) will not re-run on prod (same filename already ran).
* **Gate counts (2c, counts only)**: users with non-null email, `email_verified=0`, `created_at >= 2026-10-05 00:00 UTC`: **0**. No admin-created staff or students in scope. 50 pre-cutoff unverified accounts remain exempt. Deploying main gates nobody.
* **Env vars (2d)**: names + intended values recorded in the checklist — `TOSHI_DEFAULT_MODE=preview`, `TOSHI_ONBOARDING_V2` off, `ROBOTS_NOINDEX` off, `EMAIL_VERIFICATION_CUTOFF` unset (blank keeps the built-in 2026-10-05 default; must NOT be bulk-copied from staging), `BACKUP_WHATSAPP_PHONE` set, plus the `FILESYSTEM_DISK` staging-copy warning.
* **Checklist (2e)**: `docs/ops/production-deploy-checklist.md` (this PR) — pre-deploy (CI green, staging-canary same SHA, snapshot, backup-opens-with-password, alerts armed, .env backed up), the classified migration table with the Toshi-reset log line to watch, the deploy POST + poll pattern, the 12 smoke tests, and rollback (revert PR first, snapshot restore last, never a code-revert-as-migration-undo).
* **Production-ready main SHA (2f)**: `6fb10eeb` — production on `dabfb80c` runs 0 pending migrations, and nothing in the `dabfb80c..6fb10eeb` path is destructive or gates any existing production user.

## Task 3 — verify and merge #980 (Lucide sidebar icons) — 2026-10-05

* 3a composer diff clean: only `mallardduck/blade-lucide-icons 2.0.24` added (lock consistent).
* 3b full suite green in fresh worktree at head `5de6b632`: 2195 tests, 0 failures, 0 errors, 4 skipped; icon-specific suites 9/9 (KaIconComponentTest + SidebarLucideIconsTest).
* 3c screenshot evidence (local run at head, staging serves main only): 4 identities x 2 viewports captured to `e2e/screenshots/sidebar-lucide-2026-10-05/` plus a finance-department clip and a collapsed-rail shot; `report.json` carries the login attestation per case. Visual review: expanded group icons all render distinct Lucide glyphs; active rows use tokens (`#14532D` desktop, `--d-icon-on-accent` white on the dark mobile drawer); Admissions = user-plus; Fees area = banknote/alert-triangle/heart-pulse/bus/books/user-plus. Only console noise is the local Pusher env-placeholder WS failure (pre-existing, unrelated).
* 3d findings (recorded, not blockers):
  1. Resting sidebar icon colour resolves to `#babdbf`, not `--d-icon-muted` (#64748B): pre-existing `public/css/admin.css:6513 .sidebar svg { color: #babdbf; }` (0,1,1) out-specifies `.ka-icon--muted` (0,1,0). Nav-row resting colour is identical to main (main's icons also computed #babdbf), so no regression; group headers moved from inline `#94A3B8` to `#babdbf` (both decorative, labels adjacent). Active-state tokens verified working. Forcing the token would regress the dark mobile drawer (\~2.6:1 vs current \~6.6:1), so merging as-is; the PR's "colour comes only from the six tokens" comment is overstated for resting rows — scope-correct fix belongs in a follow-up.
  2. `heroicon-ui` remains at `resources/views/layouts/partials/navigation.blade.php:67` (top-bar hamburger) — PR body scopes to "the sidebar part of I2", so out of scope; follow-up for the top-bar part of I2.
  3. Group-header icons kept (16px muted) matching this brief's checklist; the 2026-10-01 handoff's I2 text-only wording is superseded here.
  4. Collapsed rail: Dashboard + 5 group-header icons + Help & Docs render at 20px (child items hidden by pre-existing `sidebar-group > ul { display:none }` collapse CSS, unchanged by this PR). Tooltips absent for admin-section rows because `config/navigation.php` never had `title` keys for them on main either (only 7 superadmin titles; PR preserved all of them) — pre-existing gap, noted for follow-up, not a regression.
* 3e: screenshots + this note committed to the PR branch; merging per brief authorisation (CI green, mergeable).

## Task 3e addendum — #980 landed and staging-verified — 2026-10-05

* Merge: [#980](https://github.com/KlassApp-Foundation/KlassApp/pull/980) squash-merged, GitHub API `merged: true`, merge commit `4cec64cf8afeb3d605cb26228dd02192c1f05cdf` (CI green on merged head `428a069b`: check/scan/test-guard all success; one EOF conflict in this file resolved main-wins + Task 3 section re-appended; regular merge, no force-push).
* Staging: push-to-deploy reached `deployment.succeeded` at `4cec64cf8`; CSS fingerprint `--d-icon-muted` live on staging (see the private repo KlassApp-Foundation/internal or the Cloud dashboard).
* Post-merge staging gate: `phase4.admin@` → `/admin/dashboard` and `phase4.teacher@` → `/teacher/dashboard` both 200 with 0 browser console errors; platform runtime logs for the visit window: 41 entries, all `info`, 0 non-info, 0 5xx (33 headless requests, all 200/302); active nav icon computed `rgb(20,83,45)` (token path) on staging; screenshots reviewed for both roles.
* Local evidence kept in `e2e/screenshots/sidebar-lucide-2026-10-05/`.

## 2026-10-05 (night shift, resumed): Task A recon, #982 merged, Task B verified (#986)

### Task A — where we stopped (recon)

* Worktrees: 36 (35 KlassApp + prunable /private/tmp/KlassApp-slack-6c-stamp). Unpushed: docs/progress commits 5741627e→2fea824f sit on local branch fix/require-verified-email-signin (above the squash-merged #975); not pushed.
* Open PRs: #984 (draft, marks import UI — CI green, updated 18:36Z), #961, #937, #936, #935, #890, #822, #821, #786, #732, #728, #682. (#982 was draft — merged tonight.)
* Staging: main a8f3603f → dd25beed; both staging deploys succeeded. No background processes. Many worktrees carry 247 uncommitted deletions each — all e2e/screenshots/\*\*.
* /tmp/stg-pw\.txt existed (32 B) — deleted; Rasta informed.
* Task state: B not started → done tonight; C watch (#984 still draft); D done (#974); E done (#981); F partial (a/e done; b/c/d/f next); G merged (#980); H partial (#982 merged, schools listed, Demo Academy kept).

### Task H — #982 + school list + Demo Academy

* \#982 merged via admin squash `dd25beed`; deploy succeeded. File: docs/research/cloud-environment-setup.md (210 lines).
* Active staging schools (12): 1 Phase 4 Roster Test Fixture; 7 Demo Academy Uganda; 38 Demo Junior School; 39 Demo Senior School; 100 Averify's School; 101 A4 Journey Primary School; 102 Kampala Journey Primary; 103 Journey O-Level Academy; 104 Journey O and A Level College; 105 Journey's School; 112 Suite's School-24; 133 Boris's School.
* Demo Academy Uganda kept — referenced by: database/seeders/{DemoAcademySeeder,Phase4RosterDemoSeeder,SchoolsTableSeeder}.php; design/system/concepts/\*\* (docs, profiles, quickstart, social, whatsapp-howto); docs-site (Shot.vue, contributing template); knowledge.md:736; progress docs.

### Task B — #986 staging verified (15/15)

* \#986: route teacher.exam.marks.template → MarksController::downloadTemplate (shared examRoster(); ExamAuthorization; xlsx default + csv variant); blade button "Download template" (non-nursery); MarksTemplateDownloadTest (312 lines). Marks import on main = M1 template only; full import = draft #984.
* Fixture (bridge, is\_test): school 162 "E2E Marks Verify", admin 712, teacher 713, students 714–716; exam 135 (P5 Blue, Mathematics). Purged after (dry+force; school GONE, 0 users left).
* Verified @375+1280: teacher login; exam list enter-marks link; Enter Marks page + template button; download = valid xlsx ("Marks Template"; registration\_number/student\_name/mark; 3 students; empty marks). 0 console errors, 0 5xx. Admin: /admin/exams shows the exam; admin marksheet xlsx downloads (valid); admin redirected off teacher-only route (by design). Evidence: /Users/mac/litellm/taskb-marks-verify/ (screenshots + xlsx + taskb-report.json).

## 2026-10-06 (night shift): Task C - marks import taken over, verified, merged; notes PR

### #984 (marks import) - finished locally and merged

* Local session took over the cloud session's PR per Rasta's update. Branch feat/marks-import-ui, commits fb423997 (feat) + f1668433 (test).
* Template is now two sheets: Marks (KLS number, student\_name, Mark (out of 100)) + Exam info (school, class and stream, subject, exam, term, maximum marks, generated, version, key = exam id + checksum). Teacher and admin routes emit the same shape; CSV keeps the grid only (warns on import).
* Importer: KLS number aliases (KLS number / KLS no / Reg no / registration number), mark column by prefix; verifies the template key and class/subject/exam before the preview; refusals name both files (e.g. "This file is for P6 Gold Science; you are importing into P6 Gold MATHEMATICS"); missing Exam info sheet warns but previews.
* KLS confirmed: users.registration\_number = KLS ID from StudentIdGeneratorService (KLS{school}{seq}), dual-stored as student\_academics.klassapp\_student\_id, unique per school. Staging count: 217 of 217 students have no KLS number (schools 1/2/7/38/39 breakdown in the PR). Backfill proposal (not run): students:seed-sequences + students:backfill-registration-numbers (dry run first).
* Tests: fail-first 26F/23P, after 49/49 in the marks suite; local guarded full suite 2250 tests, 0 failures, 4 skips. CI green. Merged (admin squash, Rasta-authorized) 718aaf78; deploy succeeded.
* Staging verification post-deploy (throwaway school 163, purged after dry+force; 24 checks all pass): download/fill/upload/preview/confirm at 375+1280; rerun changes nothing; wrong-exam refused; out-of-range + blank + unknown-KLS reported; legacy CSV warns; overwrite opt-in; locked blocks; cross-teacher 403; admin import works. Note: marks entry page never prefilled saved values (pre-existing, same on main); verified via the marks view page.
* PR #984 body updated with results. Evidence in /Users/mac/litellm/taskc-marks-import/ and .openclaw/tmp/taskc/.

### Notes PR

* \#987 docs(proposals): marks collection notes and class-teacher grid proposal (docs/proposals/2026-10-05-marks-collection-notes.md). test-guard green; Root Clutter Guard cancelled on infra, rerun requested.

### Housekeeping

* /tmp/stg-pw\.txt deleted (Task A). Local guard + CI evidence above. Staging schools after tonight: 12 active (unchanged list from Task A report).

## 2026-10-06 (night shift, cont.): Task F read-only results, Task G icons pass, Task H wrap

### Task F - production readiness (read-only checks only)

* Staging serves main cf8108ab (deploy succeeded; prior commit `718aaf78`). Production-ready SHA: cf8108ab.
* Migrations: production has 213 applied; 11 pending when main deploys: create\_teacher\_invites, create\_demo\_requests, create\_co\_admin\_invites, add\_ugandan\_admission\_fields\_to\_admissions, make\_legacy\_admission\_columns\_nullable, add\_attempts\_and\_locked\_until\_to\_authentications, map\_school\_student\_size\_to\_new\_buckets (DATA, irreversible), add\_toshi\_mode\_to\_schools (structure + mode per flag), change\_schools\_toshi\_mode\_default\_to\_onboarding (default only), backfill\_existing\_schools\_toshi\_safe\_defaults (DATA - the existing-schools Toshi reset; JSON capture to storage/logs + down()), add\_onboarding\_finish\_and\_skipped\_steps\_to\_schools. No gate backfill migration exists (gate is code-only).
* Gate count on production (counts only): 0 users with non-null email, email\_verified=0 and created\_at >= 2026-10-05 00:00 UTC. 40 pre-cutoff unverified, all exempt. Nothing to backfill.
* Env names: TOSHI\_DEFAULT\_MODE=preview; TOSHI\_ONBOARDING\_V2 unset/false in production (staging has true - do not copy); ROBOTS\_NOINDEX unset/false; EMAIL\_VERIFICATION\_CUTOFF blank (added since #951; per .env.example "must not be set in production for now").
* Checklist doc already merged (#983).

### Task G - #980 Lucide sidebar staging pass (16/16)

* Admin sidebar 25/25 and teacher sidebar 12/12 links carry svg icons at 375 and 1280 (plus inner pages: /admin/students, /teacher/exam/marks). Students vs Teachers icons differ; fee/students/teachers/admissions pairwise distinct; six --d-icon tokens wired; menu targets 50px at 1280; 0 console errors; 0 5xx. Screenshots: /Users/mac/litellm/taskc-icons/. Fixture school 164 purged (dry + force).

### Task H wrap

* \#982 merged dd25beed (cloud-environment-setup doc). Active staging schools (12): Phase 4 Roster Test Fixture; Demo Academy Uganda; Demo Junior School; Demo Senior School; Averify's School; A4 Journey Primary School; Kampala Journey Primary; Journey O-Level Academy; Journey O and A Level College; Journey's School; Suite's School-24; Boris's School.
* Demo Academy Uganda kept: referenced by database/seeders/{DemoAcademySeeder,Phase4RosterDemoSeeder,SchoolsTableSeeder}.php; design/system/\*\*; docs-site templates; knowledge.md; progress docs.

### Wrap notes

* /tmp/stg-pw\.txt deleted. Local guarded suite green (2250/0/0/4). All fixtures purged (163, 164). All temp credential files removed.

## 2026-10-06 (night shift, cont.): KLS at creation; marks follow-up; migration safety

### Item 1 - KLS numbers at every student-creation path (PR #988)

* Audit: engine path (wizard one-by-one, wizard paste, Toshi, registry step) already assigned via StudentIdGeneratorService and synced student\_academics.klassapp\_student\_id. Fixed: RegisterUser::CreateUser (Excel/CSV import + admin Add Student), AdmissionUser::CreateStudent (legacy date('YmdHis') crashed on the model guard; also its fee inserts referenced dropped columns - repaired to the current fee\_payments schema), seeders (DemoAcademy/DemoJunior/DemoSenior/Phase4/UsersStudent/StudentAcademic), the report-template thumbnail command, and AssignStudentsToStreamTool (was minting a second number). New helper StudentIdGeneratorService::ensureForStudent.
* Tests: tests/Feature/Students/StudentCreationAssignsKlsTest.php - 7 tests / 196 assertions; fail-first evidence captured. Local guard on the branch: 2257 tests / 0 failures / 4 skips.
* STAGING backfill (staging only): students:seed-sequences (nothing to seed) then students:backfill-registration-numbers dry-run (217 students with neither id) then run: 217 fresh IDs. Verified: missing 0/217, user-vs-academic mismatch 0, samples KLS0070001.. KLS0010001. One student has no academic row yet (id stays on the user until enrolled).
* Finding (follow-up, not fixed): SiteHelper::getStandardLinkList caches on a deterministic key with CACHE\_STORE=file in tests; a stale file cache poisons StandardLinkListHelperTest in a used worktree (cache:clear fixes; CI fresh - unaffected).

### Item 2 - marks import follow-up (PR #989)

* Result page links to the marks view (teacher: teacher.exam.marks.view; admin: marks area filtered to class/type/term).
* Entry page shows "N marks already saved" + view link only when marks exist; no prefill, entry form behaviour unchanged (explicit no-prefill test).
* Prefill proposal: docs/proposals/2026-10-06-marks-entry-prefill.md (risks + suggested shape). Marks suite: 52 passed.

### Item 3 - migration safety (PR #990)

* Toshi reset capture now mirrors to the application log via Log::info (ids + old toshi\_enabled/toshi\_mode only) in addition to storage/logs; restore list unchanged for down().
* tests/Feature/Migrations/DataMigrationSafetyTest.php: capture reaches the app log; student-size map runs safely over null/empty/500+/1000+/free text/whitespace/already-new (never throws, unknown values unchanged). 2 tests / 22 assertions.

## 2026-10-06 (final gate): items 4-5

### Item 4 - six journeys on the final tip

* Production-ready SHA: 328eaddc (tip including #988, #989, #990, #991, #992; staging deploy succeeded on it).
* Fresh detached worktree at 328eaddc, suite run against staging: 12 passed (26.0m), 0 failed, 0 skipped.
* Results (no failed checks anywhere; findings are informational):

  | journey        | 1280                                 | 375                                  |
  | -------------- | ------------------------------------ | ------------------------------------ |
  | primary-manual | pass, 0 failed, 0 findings           | pass, 0 failed, 0 findings           |
  | olevel-manual  | pass, 0 failed, 0 findings           | pass, 0 failed, 0 findings           |
  | oalevel-manual | pass, 0 failed, 2 findings           | pass, 0 failed, 2 findings           |
  | primary-toshi  | pass, 13 turns, 0 failed, 2 findings | pass, 13 turns, 0 failed, 2 findings |
  | olevel-toshi   | pass, 12 turns, 0 failed, 2 findings | pass, 13 turns, 0 failed, 2 findings |
  | oalevel-toshi  | pass, 13 turns, 0 failed, 4 findings | pass, 13 turns, 0 failed, 4 findings |
* E2E schools 166-177 purged after the run (dry + force); 0 remain on staging.
* Checklist doc updated and merged (#991): final migration list (11, none destructive), the two data migrations flagged, the snapshot-is-the-rollback line, and the Toshi reset capture now reaching the application log stream.

### Item 5 - housekeeping

* Worktrees pruned: 32 removed via scripted pass + individual handling (every one verified: merged PR, no local-only commits; held-back squash artifacts pushed to backup branches first: backup/purge-fk-local-1006, backup/mail-otp-local-1006, backup/softlaunch-1e-local-1006; script logs in .openclaw/tmp/taskc/prune-log.txt). The /private/tmp stub was pruned.
* Remaining 7 worktrees: main; wt-alevel (#936 open); wt-design (#935); wt-roles-tasks (#937); wt-sl-898 (#898); wt-finalgate (evidence for this gate); KlassApp-wt-softlaunch-1g (one docs stamp commit not on main - left for a keep/push decision).
* Local-only docs commits on fix/require-verified-email-signin: resolved - pushed and merged via #992 (night-shift logs) plus this final section.
* 46 legacy git stashes remain untouched (they predate tonight; suggest a bulk review later).

## 2026-10-06 (early): dashboard v2 finished + hotfixes + class-list proposal and deferred list

### Task 1 - admin dashboard v2 (#961)

* Recon of the draft vs Part B and dashboard-v2.html: the draft lacked the single OnboardingStepsService count (hardcoded 7), per-user DB dismissal (used the session), the sidebar chip, prerequisite naming/links on quick actions, the zero-students empty state, the academic-year select and the three Toshi states; it also carried a now-redundant Toshi::enabled()/@toshi switch that main's ToshiUiSwitch supersedes.
* Finished and rebased (forced onto the draft branch, old tip archived at backup/dashv2-initial-1006): OnboardingStepsService::progress() (one count for banner + chip + prereqs), user\_preferences table + GET /dismiss-onboarding writes it, new sidebar chip partial above the account trigger, wizard ?step= deep links, quick-action prereqs in amber linking to the exact step, snapshot empty state, real labelled 44px year select, Toshi states (onboarding: Set up with Toshi; preview: Coming soon; assistant: Early access pill only; nothing auto-opens). Icons (#980) and the account card kept + asserted.
* 12 tests / 64 assertions; local guarded suite 2274 tests / 0 failures / 4 skips; CI green; merged cc60514c.
* Deploy hotfixes: first deploy failed on the new migration (MySQL 3780, BIGINT vs INT UNSIGNED users.id), the retry failed on the half-created table (1050). #994 fixed the FK type; #995 made the migration self-healing (dropIfExists + clean create; table was empty and never shipped). Deploy 5631a023 succeeded.
* Staging verification (?v2=1, flag stays off): 52/52 checks across brand-new, mid-setup, Demo Junior and Demo Senior at 375/768/1280 - counts match the service (1/13, 8/16, 11/16), dismissal persists after reload with the chip carrying the same count, Toshi preview shows Coming soon only, no horizontal scroll, zero console errors, zero 5xx. Screenshots in /Users/mac/litellm/taskc-dashv2. Temporary demo-check admins deactivated; fixture schools purged.
* To enable site-wide: DASHBOARD\_V2\_ENABLED=true (default off; ?v2=1 works per request).

### Tasks 2-3 (this PR)

* docs/proposals/2026-10-06-class-list-confirmation.md: roster portal today (ClassRoster Index/Show + RosterScopeService file and line) and the proposed "Confirm class list" action (class teacher stream-first or admin; KLS numbers; flags; storage; resets via roster hash; printable list; marks-import notice only). Not built.
* Deploy checklist: post-deploy smoke item 13 (approve one test admission on a throwaway school, then purge) + migration count updated to 12 with the user\_preferences row.
* knowledge.md deferred backlog (2026-10-06): further admission work and form polish; SiteHelper::getStandardList stale-cache note; 46 legacy stashes; staff plan-limit notice (teachers bucket); marks entry prefill (proposal on main).

### Merges after 328eaddc (rule 10 tracking for the final gate)

* 67dc9127 (#993 docs), cc60514c (#961 dashboard v2), 35368c04 (#994 FK fix), 5631a023 (#995 migration recovery), plus this docs PR.

## 2026-10-06 (gate): final six-journey gate + dashboard checks on 355e89bb

* Final tip after Tasks 1-3: **355e89bb**. Everything merged after 328eaddc: 67dc9127 (docs), cc60514c (dashboard v2), 35368c04 (FK type fix), 5631a023 (migration recovery), 355e89bb (docs: proposal + checklist + deferred). This entry's PR follows.
* Journeys (dashboard flag off, fresh worktree pinned to 355e89bb, 12 runs): 10 passed in the batch; 2 signup-wait timeouts (olevel-toshi 1280 and primary-manual 375) happened while the dashboard checks were running in parallel; both passed on isolated reruns with 0 failed checks. Load flake; do not run the gate journeys in parallel with other browser suites.
* Dashboard checks (?v2=1) on the same commit: 52/52 across brand-new, mid-setup, Demo Junior and Demo Senior at 375/768/1280 - counts match the service, dismissal persists after reload with matching chip, Toshi shows the preview state only, no horizontal scroll, zero console errors, zero 5xx.
* Gate fixtures purged (dry + force; 14 E2E schools 180-195 removed; 0 remain). Demo-check admins deactivated; Demo Junior/Senior intact. Staging students without a KLS number: 0.
* Deploy checklist migration list still accurate: 12 pending for production (includes user\_preferences, now self-healing since #995).
* DASHBOARD\_V2\_ENABLED remains unset (default off); ?v2=1 works per request; set DASHBOARD\_V2\_ENABLED=true to turn it on.

## 2026-10-06 (late night): legacy school cleanup + MySQL migration proof

### Task 1 - legacy staging schools

* Reported the full active-school table (id, name, created, users, students, is\_test, last activity, admin domain only). The eight approved: six failed the guard (100-105 admin addresses are klassapp.xyz with non-e2e local parts; 133 is gmail.com) and were skipped as instructed; 112 (e2e local part) qualified and was removed. Login condition used actual session logins: none of the eight has any session activity; only creation-day remember-token rows exist.
* Also cleared my own five stragglers from the night gate runs (186/188 failed-signup leftovers; 196-198 signup smokes) with the same rename + is\_test + purge method (dry run first).
* Verify: 11 active schools remain (4 keep-list + 6 skipped + Boris's); Mucu SS intact (inactive); 0 students without KLS; sign-up smoke passed end to end (register to verify code from staging logs to dashboard); demo login passed (Demo Junior, temp admin deactivated after).

### Task 2 - user\_preferences on the clean MySQL path

* Production (read-only, information\_schema): users.id is int unsigned, NOT NULL, PK, auto\_increment. Staging identical, so no type drift.
* Staging: user\_preferences was alone in batch 21; migrate:rollback --step=1 rolled back exactly it (24.99ms); table confirmed gone; php artisan migrate re-ran the clean create path on MySQL (120.75ms); structure verified (user\_id int unsigned, unique user\_id+key, foreign key to users); a second migrate reported "Nothing to migrate". Repeat-safe.
* Proposal for a pre-deploy MySQL guard: docs/proposals/2026-10-06-mysql-migration-guard.md.
* Schema changes since 328eaddc: only the user\_preferences migration (confirmed by diff); checklist carries the new line about MySQL checks for foreign-key migrations; migration list still 12.

## 2026-10-06: demo rich data + demo:refresh (PR #999, draft until CI)

* Branch `feat/demo-rich-data`, draft PR [#999](https://github.com/KlassApp-Foundation/KlassApp/pull/999). Work secured before the refresh fix (`83aba047`), then `1a205b6e` and `48e2f412`.
* Refresh root cause: `User` uses soft deletes, so `delete()` left the added row in `users` and the assertion correctly still saw it. The cutoff is the manifest `seeded_at` captured before the seeder rewrites it; test rows are created 10 seconds after that instant because a same-second `created_at` is not greater than the cutoff.
* `SKIP teacher` / `Inserted 0, skipped 50` is the existing migration `2026_08_12_164737_import_teacher_subject_assignments` echoing while the test database is built. It is not refresh output.
* Isolation (FK pragma on): another school is untouched; a non-demo school is refused; every pre-seed password hash and admin row is unchanged; a listener test failed first on nine deletes that filtered only by user or exam id, then every delete includes `school_id`.
* Local guarded suite on `48e2f412`: 2290 tests, 0 failures, 4 skipped. Staging `demo:refresh` is next, after this PR is on staging.

## 2026-10-06 (night): batch-one Part B — groups 1-5 merged; group 6 next

* **#1007 merged** `96328939` — academic year updates scoped to the school; staging deploy succeeded. Groups 1-4 merged earlier tonight: #1003 `64e07340`, #1004 `ccc7c5ab`, #1005 `a2ee6566`, #1006 `6c7520a3`.
* **Next:** Group 6 — dashboard fees/reminders scoping; then group 7 (approvals), group 8 (admissions), the batched staging pass (all eight areas, one throwaway school, 375+1280, purge after), Tasks C/D/E, the full local suite, and the final six-journey gate on the final commit.

## 2026-10-06 (night 2): batch-one Part B - groups 6 and 7 merged; group 8 next

* **#1010 merged** `adc9b751` - dashboard fees/reminders scoped to the school; staging deploy succeeded.
* **#1011 merged** `e1bc8126` - admin approvals deny-by-default and school-scoped.
* GitBook statuses on #1009 never updated (stuck on the GitBook side); that PR was merged on green check/scan/test-guard. Flagged for the integration owner.
* **Next:** Group 8 (admissions) PR and merge; then the batched staging pass (all eight areas, one throwaway school, 375+1280, purge after), Tasks C/D/E, the full local suite, and the final six-journey gate on the final commit.

## 2026-10-06 (night 3): group 8 merged - all eight P1 groups complete; staging pass next

* **#1013 merged** `a2623995` - admissions scoped to the caller's school (list/show/edit/update/delete; grouped OR conditions; the #988-repaired approval flow kept working).
* All eight P1 groups are now merged and deploy-verified: #1003, #1004, #1005, #1006, #1007, #1010, #1011, #1013.
* **Next:** the single batched staging pass (all eight areas, one throwaway school, 375+1280, purge after), then Tasks C/D/E, the full local suite, and the final six-journey gate on the final commit.

## 2026-10-07 (early): staging pass complete; duplicate-rule fix merged

* **Staging pass (batched, all eight areas, ONE throwaway school):** loads - 8 areas x 375/1280 all 200 with zero console errors and zero 5xx; saves verified at both widths (fee categories create/edit, grading create, promotion create, fee payment record, academic-year edit).
* One 500 found: creating a duplicate promotion rule. **#1015 merged** `0db58e0c` (duplicate combos now show a form error; re-verified on staging, no 5xx). Throwaway school purged (dry + force).
* **Next:** Tasks C/D/E, the full local suite (running at time of writing), and the final six-journey gate on the final commit.

## 2026-10-07 (later): spreadsheet imports now attach students to their class

* **#1017 merged** `fd9ec31c` (deploy ) — students imported from a spreadsheet now attach to the class named in the sheet ("Senior One" style wording and short codes like "S.1" both resolve). Found during the class-setup walkthrough: secondary-school imports were silently creating classless students. Re-verified on staging: fresh import -> linked, KLS minted, class roster lists them.
* **Next:** final six-journey gate on the final commit, then the wrap-up report.

## 2026-10-07 (final gate): six journeys, both widths

* Production-ready SHA: `db56e7cf` (tip including #1003-#1018); staging deploy succeeded on it.
* Fresh suite run against staging on the final commit: **12 passed / 12 journeys (0 failed, 0 skipped)** — all six journeys on desktop-1280 and mobile-375.
* Results (no failed checks anywhere; findings are informational):

  | journey        | 1280                                 | 375                                  |
  | -------------- | ------------------------------------ | ------------------------------------ |
  | primary-manual | pass, 0 failed, 0 findings           | pass, 0 failed, 0 findings           |
  | olevel-manual  | pass, 0 failed, 0 findings           | pass, 0 failed, 0 findings           |
  | oalevel-manual | pass, 0 failed, 2 findings           | pass, 0 failed, 2 findings           |
  | primary-toshi  | pass, 13 turns, 0 failed, 2 findings | pass, 13 turns, 0 failed, 2 findings |
  | olevel-toshi   | pass, 13 turns, 0 failed, 2 findings | pass, 13 turns, 0 failed, 2 findings |
  | oalevel-toshi  | pass, 13 turns, 0 failed, 4 findings | pass, 13 turns, 0 failed, 4 findings |
* All 15 E2E schools purged after the run (dry + force); 0 remain on staging.
* Housekeeping: the batch-one scheduler jobs (status check, Part B kickoff, Group 4 kickoff) removed; full local suite result stands at 2352 tests / 0 failures / 0 errors / 4 skipped.

## 2026-10-08 (MVP batch): role crawl, MVP journey, fixes, production counts

* **Demo-school role crawl (Task D)**: 8 roles at Demo Junior + Senior, 375 and 1280, read-only; every role's navigation reached. Findings registered (head: classes page refused, settings link 404; subadmin console 404s; missing calendar asset; teacher libraryactivity redirect). No server errors.
* **MVP path journey** (new, `@mvp-endtoend`): signup+code+wizard -> teacher invites + stream + assignments -> 5 students with parents (KLS visible on list/roster/overview) -> invite acceptance + attendance -> one EOT exam marked by form AND spreadsheet import -> report-card PDF verified from the file (name + mark) -> bursar payment + balance + fee reminder -> parent sees child's grades and fee balance. Screenshots at every step: `e2e/artifacts/mvp/`.
* **Fixes merged** (each failing-test-first, CI green, staging-verified): #1020 `b78be9e6` (fee reminders carry a real message; bursar route), #1021 `20349d3f` (KLS numbers on the three surfaces), #1022 `26c8ae3c` (exam create no longer 500s on an empty teacher), #1023 `f6ca3f5a` (bursar balance strip), #1024 `fe5fab73` (the journey), #1025 `9dea2758` (deploy checklist + MVP smoke), #1026 `509a7100` (mobile marks-submit hardening). Deploys succeeded: , , , , , , .
* **Read-only investigations**: the sporadic 419 is a stale-token artifact of rapid retries (users otherwise get the guided "refresh and try again" page); an interrupted signup is recoverable — signing in again shows the setup banner and the wizard can be resumed (verified live).
* **Production counts (read-only): all zero** — no NULL-school rows in subscriptions / school\_grading\_systems / student\_promotion\_rules / fees\_categories, no admin-group users without a school, no subscription/user school mismatches -> the route scoping hides nobody's own records.
* **Final run**: six journeys + MVP journey, both widths, on `509a7100` — **14 passed (45.6m), 0 failed** (desktop and mobile: 3 manual + MVP + 3 Toshi each). GATE3\_EXIT=0.
* All E2E schools purged after the run (dry + force); 0 remain.

## 2026-10-08 (later): addendum round — role-crawl fixes + helper guards; handover

* **#1028 merged** `0fc6da82` (deploy ) — role-crawl quick fixes (PR A5):
  * SchoolSubadmin nav endpoints (year list, notification feed) returned 404 because `routes/setting.php` registered duplicates *after* the admin routes, shadowing them with `fullschooladmin`. Duplicates removed; both now 200 for ug4 and for admins (verified live on staging).
  * The shared admin sidebar hides **Settings** and **Classes & Streams** from SchoolSubadmin — its route-level rules exclude ug4 (Settings by design; the classes roster portal's scope service hard-denies outside admin/teacher). Implemented as a reusable `school_admin` nav condition across the grouped + top renderers (empty groups skipped), and the subadmin dashboard's two direct links into the same 403 are neutralised.
  * Teacher sidebar no longer links the unbuilt library-activity page; the `/uploads/calendar.png` asset the stylesheet references now exists.
  * Evidence: failing-first 5F/2P → 7P; Navigation 33, roster 6, subadmin access 6, route-health 7 — all green; staging re-check as head/teacher/admin passed.
* **#1029 merged** `162bbdb1` — staging helper guards: `outcomes.setTestFlag` and `outcomes.enableOnboardingMode` now refuse `is_demo` schools (return `refusing-demo-school`).
* **Step 0 verification (addendum)**: schools 1 / 7 / 38 / 39 have correct names + flags — 1 = "Phase 4 Roster Test Fixture" (is\_test only); 7/38/39 = is\_demo + is\_test. Helper inventory: app commands already flag-guard themselves (`test:purge-schools`, `demo:purge-schools`, `demo:refresh`, `demo:set-password`); the e2e helpers are guarded by #1029; the local (non-repo) purge helper that over-matched on 2026-10-08 was replaced with a scoped, no-rename version.
* **Handover to the design-build session** (it holds the full build prompt; this session stops here):
  * Sequence rules: after EVERY build PR, run the affected journeys (and the MVP journey) on staging and update their selectors in the same PR — keep the suite green throughout. Step 7 re-runs all 14 runs (six journeys + MVP journey, both widths); do NOT rebuild the MVP journey.
  * A6 ("Send reminder" action on the fees page, confirmation dialog naming parent count, uses the #1020 endpoint, never sends for demo/test schools, shows sent/skipped) lands after PR4 and is added to the MVP journey.
  * Parents' report-card PDF: register entry only — do not build.
  * Toshi dock must not cover footer actions on teacher pages at 375 — verify after A2.
  * If demo-critical PRs are at risk, cut in order: B3, B2, B1, B4, PR6, PR5 — never cut Steps 1-5, A5, A6 or Step 7.
  * Open register items from the crawl: subadmin 404 endpoints + head classes/settings links + calendar asset + teacher library link are all **fixed/hidden by #1028**; the remaining crawl items live in `reports/klassapp-mvp-batch-report-2026-10-08.md`.

### Addendum 2 (2026-10-08, from Rasta) — to bundle with the dashboard/design work

Received with main at `2a22067c` (#1028/#1029/#1030 merged + deployed; demo schools verified). Step 0's school check and A5 stay closed.

* **PR1 additionally fixes K25**: the setup bar and the setup chip take neutral step names from ONE source (e.g. "Registration code", "Exam centre number"); curriculum-specific names appear only when the school's curriculum defines them; the bar lists only the steps that are actually incomplete.
* **Sidebar groups for the demo (confirmed by Rasta)**: Dashboard; People (Students, Teachers and staff, Parents); Academics (Classes and streams, Subjects, Attendance, Exams and marks, Report cards); Money (Fees); Messages (WhatsApp); School (Settings, Help). Every existing menu item stays reachable — the PR body must list which items moved and where.
* **Ordering**: PR7 (account menu popover; the sidebar never moves) is built directly after PR1 — before PR2 — because the bug shows on the dashboard shell.
* **Flag rollout**: after PR1 merges, report the exact environment variable and value to set on staging: **`DASHBOARD_V2_ENABLED=true`** (`config/dashboard.php` already reads it; default off), and verify the dashboard in both flag states.
* **Cut order (updated)**: B3, B2, B1, B4, PR6, PR5 — never cut Steps 1-5, A5, A6, Step 7, **PR7**.

## 2026-10-08 — design build (demo 17 Oct): session log

* Design pack committed: `design/system/` refreshed to the 2026-10-08 combined export (admin MVP pack: PR0–PR7 handoff, `concepts/admin-mvp/`, batch-two handoff); no files removed.
* **PR0 (#1033):** teachers list opens Edit/Manage by display name (school-scoped lookup); failing-first, cross-school 404 test included.
* Design cleanup: superseded admin handoffs removed (`handoff-2026-10-07-admin-mvp.md`, `handoff-2026-10-08-admin-batch2.md`); readme repointed to the combined handoff; nothing else referenced them.
* **A1:** demo data completed — every student has a parent (1–3 children each; 40 parents per school), DOB + gender for every student, fictional phones for teachers, and all setup steps complete (no "Finish school setup"); `demo:refresh` reseeds with the same rules.
* **A2:** the floating "Toshi Agent · Talk" pill and the dashboard product-demo's Toshi slide render only in assistant mode (new `@toshiAssistantMode` blade condition, mode-based via `ToshiUiSwitch`); preview/onboarding schools keep the quiet dock only.
* **Demo report cards:** both demo schools now carry the previous term's report cards generated through the app's own pipeline (merged, openable PDFs in Recent generations) while the current term stays ungenerated, so "Generate report cards" works live; the per-school currency setting is seeded and `demo:refresh` preserves the state.
* **Toshi checklist:** the panel's checklist and progress (dock and maximized sidebar) now read the one `OnboardingStepsService` source — same count and neutral labels as the dashboard setup bar and the sidebar chip, and every row opens the same step. The private `AgentToshi` step array remains only as the legacy flow navigator (every remaining call site is listed in the PR).
* **PR1 (#1037):** the admin first screen — header with term + academic-year selector; a slim setup bar (one count + neutral step names from `OnboardingStepsService`; ✕ moves progress into the sidebar chip); five KPI tiles; four charts (per-class, attendance 8 weeks, gender incl. "Not specified", fees by month collected vs expected); recent activity from real events; a brand-new state that stays minimal. No promo carousel, no Connected-tools card, no Toshi slide. The admin sidebar is regrouped to the confirmed People / Academics / Money / Messages / School list (moves listed in the PR body; every pre-PR1 item still reachable).

## 2026-10-10 — audit fix round (shell, dashboard, teacher edit, report cards)

* PR A (#1051, `fix/audit-shell-dashboard`): sidebar pitch 48px, account menu portaled to the document body, greeting in the school country timezone, distinct quick-action and KPI icons, attendance weeks from the current term, one performance bar per class.
* PR B (`fix/audit-teacher-reports`): teacher profile save persists designation, class report cards keep Generate and add Download PDF when a merged file is ready, neutral school labels, Saturday attendance for the walkthrough teacher, and the retired droplet address removed from this file's sibling `knowledge.md`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://klassdocs.gitbook.io/klassapp-documentation/docs/research/progress-2026-10-03.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
