# TFB v1.6.1 Production Stability Report

Date: 2026-08-17  
Package: `tfb-engine-v1.6.1-production-stability.zip`

## Root causes fixed

- **EVID1:** `SecondaryContentRepository::listForTest()` queried nullable channel-reference columns before some deployed databases had run their migration. `Migrator::upgrade()` now guarantees the columns and `(test_id, sort_order, id)` index idempotently; install/update/restore all converge through this Migrator.
- **EVID2:** `QuizFlowHandler::goBackOneQuestion()` called nonexistent `QuestionRepository::database()`. Rewind now uses `SessionService` + `SessionAnswerRepository`, removes the latest answer and stale aggregate scores, resets the session question and uses the normal edit/replace renderer. Other real `->database()` same-bug hits were removed.
- **EVID3:** raw Telegram edit failures were logged before callers could classify them. `TelegramClient` now classifies not-modified as success, classifies uneditable/inline-keyboard failures without ProblemsLog noise, and provides one-send fallback helpers. Admin and student pointers are updated to fallback message IDs.
- **EVID4/EVID5:** `public/index.php` and `UpdateRouter` wrote many stage lines for every success; malformed requests repeated warnings. Stage/FIRST-LINE tracing is now debug-only, success requests default to no lines, warnings are throttled for 60 seconds, and logs rotate at 512KB with three generations.
- **Update hang:** update/downgrade had no operation lock/maintenance snapshot, assumed an unwrapped ZIP, migrated before code apply, and had early returns outside common cleanup. The flow now has confirm, safety backup, wrapper/manifest detection, denylist, TTL lock, maintenance snapshot, apply→migrate→verify, and terminal workspace success/failure under `finally` cleanup.

## Update state machine

`confirm → safety_backup → wait_zip → validate → lock+maintenance → extract/preflight → apply_files → migrate_current → verify(pending=0/classes/boot) → terminal_success|terminal_fail → restore_maintenance+unlock`

The package root detector accepts root-level or one wrapper folder containing `public/index.php` and `src/`. Ambiguous roots, traversal, absolute paths and symlinks are rejected. Live config, storage, install locks, env files and private keys are denied from swap.

## Backup / restore

- Backup ZIP: `database.sql` + `backup-meta.json` (`format=tfb_backup_meta`, schema version 1, app/schema revision, time, PHP version, hashed DB name; no raw secrets).
- SQL rows are exported in batches; logs are excluded.
- Restore accepts current backup ZIP or legacy SQL, requires the Persian confirmation phrase, creates a safety backup first, enables maintenance, streams SQL with FK checks disabled only for import, then always runs the current Migrator twice and requires the second pass to be empty.
- Future schema revisions are refused. Failure remains terminal and references the safety artifact.

## Test export/import

`format=tfb_test_export`, `schema_version=1`. Export includes test flags/gate, profiles, results/media, questions/options/branches/scores and secondary items. It excludes users, sessions, answers, analytics, campaigns, broadcasts, reminders and counters. Import is admin-only, local JSON only, max 2MB, transactional, creates an inactive new test, remaps IDs/scores/media, suffixes colliding slugs and preserves the original.

## Log defaults

- Production stage trace: off; enabled only by `TFB_DEBUG` or app debug.
- Normal level: warning/error according to existing setting.
- File cap: 512KB.
- Retained rotations: 3.
- Identical warning throttle: one per 60 seconds.
- Real exceptions: app + error + problems aggregation.
- Telegram not-modified: benign success, not a problem.

## Changed files

Code/config:
- `config/app.example.php`
- `public/index.php`
- `src/Bot/TelegramClient.php`, `UpdateRouter.php`, `WebhookKernel.php`
- `src/Controllers/Admin/AdminBackupController.php`, `AdminBroadcastController.php`, `AdminCallbackDispatcher.php`, `AdminMessageDispatcher.php`, `AdminReplier.php`, `AdminSettingsController.php`, `AdminTestController.php`, `AdminUpdateController.php`
- `src/Controllers/System/SystemController.php`
- `src/Core/App.php`
- `src/Engines/Flow/QuizFlowHandler.php`
- `src/Install/Installer.php`, `Migrator.php`, `Seeder.php`
- `src/Logging/FileLogger.php`
- `src/Repositories/SessionAnswerRepository.php`, `UserRepository.php`
- `src/Services/BackupService.php`, `SessionService.php`, `TestTransferService.php`, `UpdateService.php`, `UserService.php`
- `src/Support/AdminEditableSettings.php`, `AdminStates.php`
- `tfb-update.json`

Tests/docs:
- `tests/mock_telegram_server.php`, `phase07_ops_e2e.php`, `test_transfer_e2e.php`
- `docs/ADMIN_GUIDE.md`, `AGENT_HANDOFF.md`, `CHANGELOG.md`, `DB_SCHEMA.md`, `KNOWN_ISSUES.md`, `OPERATIONS.md`, `PROJECT_STATUS.md`, `STATUS.json`, `UPDATE_ROLLBACK.md`, this report

## Migrations / revisions

- Current revision: `1.6.1`.
- Guaranteed secondary steps: `caption_text`, `channel_chat_id`, `channel_message_id`, `idx_secondary_test_sort`.
- `schema_migrations` receives the current revision once even when no DDL is pending.
- Fresh Installer executes `Schema::create + Seeder + Migrator`; update/restore execute the same Migrator and require rerun=0.

## Acceptance

### A Runtime P0
| ID | Result |
|---|---|
| A1 secondary SQL crash gone | PASS |
| A2 secondary ON entry | PASS |
| A3 secondary OFF quiz | PASS |
| A4 back no fatal | PASS |
| A5 back enabled correct | PASS |
| A6 not-modified soft | PASS |
| A7 can't-edit fallback + pointers | PASS |
| A8 normal inline markup edits | PASS |
| A9 callback spinner/ack | PASS |

### B Logs/Host
| ID | Result |
|---|---|
| B1 compact success logs | PASS |
| B2 512KB×3 rotation | PASS |
| B3 junk JSON minimal | PASS |
| B4 real errors visible | PASS |
| B5 no secrets logged | PASS |

### C Performance
| ID | Result |
|---|---|
| C1 one lazy PDO per request | PASS |
| C2 hot-path hygiene | PASS |
| C3 indexes | PASS |
| C4 delay/retry clamps | PASS |
| C5 secure htaccess retained/public route allowed | PASS |

### D Export/Import
| ID | Result |
|---|---|
| D1 full structural export | PASS |
| D2 runtime/stat exclusion | PASS |
| D3 imported test playable E2E | PASS |
| D4 slug collision | PASS |
| D5 invalid JSON error | PASS |
| D6 original unchanged | PASS |
| D7 prompt/upload cleanup | PASS |
| D8 media/secondary refs + warnings | PASS |

### E In-bot update
| ID | Result |
|---|---|
| E1 confirm UX | PASS |
| E2 safety backup | PASS |
| E3 evil/ambiguous ZIP rejection | PASS |
| E4 config hash preserved | PASS |
| E5 files applied | PASS |
| E6 migrations/schema_ok | PASS |
| E7 terminal success | PASS |
| E8 terminal failure + unlock | PASS |
| E9 admin/start/health after apply | PASS |
| E10 maintenance restored | PASS |
| E11 dogfood wrapped layout accepted | PASS |

### F Backup/Restore
| ID | Result |
|---|---|
| F1 create/list/delete | PASS |
| F2 metadata schema revision | PASS |
| F3 current ZIP restore | PASS |
| F4 old SQL forward migration + data | PASS |
| F5 safety before restore | PASS |
| F6 terminal states | PASS |
| F7 future backup refused | PASS |
| F8 streamed typical dump | PASS |

### G Downgrade
| ID | Result |
|---|---|
| G1 terminal success/fail; no hang | PASS |
| G2 safety backup | PASS |
| G3 secrets preserved | PASS |
| G4 post-health/schema conditions | PASS |

### H Install
| ID | Result |
|---|---|
| H1 fresh/upgraded revision convergence | PASS |
| H2 install lock/self-cleanup | PASS |
| H3 no secret leak | PASS |

### I Existing product regression
| ID | Result |
|---|---|
| I1 bare start matrix | PASS |
| I2 test deep link default off | PASS |
| I3 campaign deep link default off | PASS |
| I4 exact free-text help | PASS |
| I5 mid-quiz text not answer | PASS |
| I6 inline callbacks score/advance | PASS |
| I7 result/media/CTA rules | PASS |
| I8 admin workspace/cleanup | PASS |
| I9 admin takes deep-link quiz | PASS |
| I10 unique retake stats | PASS |
| I11 DB non-members broadcast | PASS |
| I12 reminder path | PASS |
| I13 health schema_ok/no secrets | PASS |

### J Agent proof
| ID | Result |
|---|---|
| J1 ordered phase gates | PASS |
| J2 honest executable proof | PASS |
| J3 root causes mapped | PASS |
| J4 surgical file set | PASS |
| J5 residual risks listed | PASS |

## Decisions locked by agent (minimal)

1. Patch version `1.6.1` was used because this is a stability/schema-operations release on the existing 1.6 line.
2. General backup artifact is DB+metadata only to stay safe on weak hosts; update/downgrade additionally create a separate code safety ZIP.
3. Downgrade remains forward-schema-only; destructive down migrations were not invented.
4. Imported tests are always inactive drafts; same-bot Telegram file IDs/channel references are preserved.

## چک‌لیست کوتاه مالک

1. ZIP را از پنل بروزرسانی انتخاب، «تأیید بروزرسانی» را بزنید و فقط پس از پیام سبز موفقیت ادامه دهید.
2. `/admin` و سپس یک `/start` بدون payload را تست کنید.
3. یک لینک تست و یک لینک کمپین را در حالت default OFF باز کنید.
4. یک آزمون secondary و یک آزمون معمولی را باز کنید؛ سپس گزینه‌های inline را تا نتیجه و CTA ادامه دهید.
5. متن «سلام» بفرستید و پاسخ دقیق help بدون دکمه را ببینید.
6. در آزمونی با back روشن، Q1→Q2→Back→Q1 را تست کنید.
7. از یک آزمون خروجی JSON بگیرید، آن را import کنید و نسخه draft جدید را بررسی کنید.
8. یک backup ZIP بسازید و وجود `database.sql` و `backup-meta.json` را بررسی کنید؛ restore را فقط روی محیط امن/با تأیید انجام دهید.
9. Health باید `schema_ok=true` و `pending_migrations=0` نشان دهد.
10. حجم logها را بعد از چند ساعت بررسی کنید؛ هر فایل باید در حدود سقف 512KB rotate شود.

## Residual risks

- Very large DB imports are memory-bounded but may hit a host execution-time limit.
- Imported Telegram file IDs generally do not work across a different bot token.
- Shared-host file copy is not an atomic filesystem transaction; safety backups and terminal verification mitigate this.
- Rare OPcache lag may require the host's normal cache reset; owner must run `/admin` and `/start` after update.
- Full code is not duplicated into every general DB backup ZIP; update/downgrade make their own code safety ZIP.
