A row the merge bug minted is visible, fixable, and repairable in bulk (#938)
#920 made a merged ticket read-only on the server and shipped NO data change, deliberately: its guard reads `status === 'merged'` and not `merged_into_id !== null`, so the rows the bug had already produced — the pointer set, the status back to `open` — stayed EDITABLE and could be corrected in the product instead of in SQL. What it left behind was rows that are simultaneously live in the All Open queue and merged into another ticket, with nothing on any screen saying so and nothing offering to fix them. Both halves of the card ship, because option A is a BEHAVIOUR RULE and a behaviour rule is a setting: B — the office fixes them one at a time. `merged_inconsistent` rides on the model (Ticket::$appends), so the list row, the index detail pane and the Show header all draw the same amber badge — "merged_into set but status open — fix?" — off one predicate rather than three copies of an expression. The badge IS the button: it opens a modal that says what will change and what will not, and posts to a new `mark-merged` verb that writes `status` and nothing else. A new "Merged" view lists every ticket folded into another one BY THE POINTER, so the consistent and the broken appear side by side; a status filter would show one or the other and never the pair worth comparing. A — the migration re-marks all of them at once, and it is INERT unless `ticketing.auto_repair_merged_on_deploy` is on. It defaults OFF, which is also what a box that has never heard of the setting does, so this can ship ahead of the operator's decision and change nothing unasked. The toggle is on the Automation tab with the live count beside it, because nobody should arm a bulk data change without seeing how many rows it touches. `MergedTicketRepair` is one implementation with three callers for one reason: "which rows are broken" and "what fixing one means" must not drift, or the badge offers a fix on a row the migration skips. THE PROD COUNT THE CARD ASKS FOR IS NOT IN HERE, and it could not be: this worker box holds no ssh private key, so central-prod is unreachable from it and there is no prod container locally. `php artisan ticketing:repair-merged --dry-run` is the read-only answer for whoever does have access — it prints the card's own SELECT COUNT and lists the rows, and `--force` (not the bare form) is the only spelling that writes. Repairing NEVER reverts. Messages, participants, assignee and title edits made to a row while it was wrongly editable are somebody's work and the only copy of it; there is no record of what the fields held before the bad save, so "reverting" would invent a previous state rather than restore one. Proved by tests/Feature/Ticketing/MergedTicketInconsistentRowsTest.php (22 tests) in dashboard-shell, over the fixture the card names — one inconsistent row beside a genuinely merged one, both produced by the real merge endpoint. The NEGCTRL half is what keeps it honest: the consistent row and an ordinary open ticket must come out of every path untouched, and a repaired row must keep the reply and the rename it was given after the merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Dashboard\Ticketing\Support\MergedTicketRepair;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* OPTION A OF [central-ticketing] #938 — the repair, behind a setting that is OFF.
|
||||
*
|
||||
* The rows the merge bug minted (`merged_into_id` set, `status` back to `open`)
|
||||
* are data, not code, and #920 deliberately shipped no data change. This
|
||||
* migration is the one that would: it re-marks every such row `merged`, which
|
||||
* makes it read-only under #920's guard, and it leaves every message,
|
||||
* participant and field edit made after the merge exactly where it is.
|
||||
*
|
||||
* IT DOES NOT RUN UNLESS THE OPERATOR HAS ASKED. `MergedTicketRepair::SETTING`
|
||||
* — "auto-repair inconsistent merged tickets on deploy" — defaults off, and a
|
||||
* box that has never heard of it therefore deploys this migration and changes
|
||||
* nothing at all. That is what lets the code ship ahead of the decision: the
|
||||
* office gets the badge and the one-click fix now (option B), and turning this
|
||||
* on is the operator's separate, later, reversible act (option C).
|
||||
*
|
||||
* WHAT IT ALWAYS DOES, EITHER WAY, IS COUNT AND SAY SO. The dry-run count goes
|
||||
* to the log on every deploy whether the repair is armed or not, because the
|
||||
* number is the thing #938 was blocked on and a deploy is the one moment
|
||||
* something is definitely looking at prod's database.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// The dry run, first and unconditionally.
|
||||
$count = MergedTicketRepair::count();
|
||||
|
||||
Log::info(sprintf(
|
||||
'[ticketing] #938 dry run: %d ticket(s) have merged_into_id set with a status other than "merged".',
|
||||
$count,
|
||||
));
|
||||
|
||||
if ($count === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! MergedTicketRepair::enabledOnDeploy()) {
|
||||
Log::info(sprintf(
|
||||
'[ticketing] #938: leaving those %d row(s) editable — "%s" is off. '
|
||||
.'Turn it on in Ticketing → Settings → Automation, or fix them one at a '
|
||||
.'time with the "Mark merged" button on each ticket.',
|
||||
$count,
|
||||
MergedTicketRepair::SETTING,
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MergedTicketRepair::repairAll('migration 2026_09_04_000001');
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no down migration, and that is a decision rather than an omission.
|
||||
*
|
||||
* Reverting would mean putting each row's status back to what it was before
|
||||
* the bad save, and nothing anywhere records what that was — the bug
|
||||
* overwrote it. Setting them all back to `open` would not be a rollback; it
|
||||
* would be re-creating the corrupt shape on rows that may have been merged
|
||||
* legitimately years ago. A row repaired in error is corrected the same way
|
||||
* any other status is: an agent edits the ticket.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Intentionally empty — see the note above.
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user