Delete channels from the UI, with a defined cascade #141

Closed
opened 2026-08-13 21:12:55 +02:00 by myrmidex · 1 comment
Owner

Problem

Channel cards on /channels offer a toggle, account management, and (since #137) an edit action — but no way to delete a channel. Since #137 deliberately left the community/instance pairing immutable, delete-and-recreate is now the only supported way to re-point a channel at a different community. That path currently does not exist in the UI.

This has a concrete trigger: the local newsbottest and news channels hold stale community ids (101 and 102, where the instance reports 217 and 8), and publishing fails with couldnt_find_community. There is no way to fix that through the interface.

The API can already do this

PlatformChannelsController::destroy() exists and calls $platformChannel->delete(). Only the Livewire UI has no path to it, so this is partly surfacing existing capability rather than building it.

The cascade graph

Enumerated by reading every migration, not just the ones the ticket originally listed:

Table FK to platform_channels Added
platform_account_channels direct, cascade 000003
routes direct, cascade 000004
keywords direct, cascade 000004
route_articles composite (feed_id, platform_channel_id)routes, cascade 000007
platform_channel_posts direct, cascade 000013
article_publications direct, cascade 000023 — added by this ticket

Five of the six already cascaded before this ticket started. Only article_publications was missing a constraint — its platform_channel_id was a bare unsignedBigInteger.

Decision: CASCADE

Orphaned publications would not actually have broken anything — PublicationsPerChannel iterates channels and looks up counts by id rather than joining, PublishSuccessRate never touches channels, and ArticlePublication has no platformChannel() relation. So this was a data-retention choice, not a breakage fix.

Chose ON DELETE CASCADE: the rows are unreachable once the channel is gone, so keeping them means carrying data nothing can read while the dashboard's totals silently disagree with a raw COUNT(*).

Accepted consequence: deleting a channel removes its route_articles and article_publications, so ApprovalRate, PublishSuccessRate and ArticlesTrend lose those data points for past days — a previously-rendered chart will change. No stat crashes.

Acceptance criteria

  • Channel cards show a delete action
  • Deleting requires confirmation, and the confirmation states what will be removed with per-channel counts
  • Cascade behaviour for article_publications decided, implemented, and documented in PLATFORM.md
  • platform_account_channels links are cleaned up rather than orphaned — already true before this ticket (000003)
  • Deleting a channel does not break the dashboard's per-channel breakdown or the publish-success-rate stat
  • Feature tests cover the delete path, the confirmation, and the full cascade

Notes

  • Related: #137 "Channel cards have no edit option" made the community pairing immutable, which is what makes delete the re-point mechanism.
  • The equivalent gap likely exists for feeds — Feeds.php has no delete path either. Worth a separate ticket rather than widening this one.
## Problem Channel cards on `/channels` offer a toggle, account management, and (since #137) an edit action — but no way to delete a channel. Since #137 deliberately left the community/instance pairing **immutable**, delete-and-recreate is now the only supported way to re-point a channel at a different community. That path currently does not exist in the UI. This has a concrete trigger: the local `newsbottest` and `news` channels hold stale community ids (101 and 102, where the instance reports 217 and 8), and publishing fails with `couldnt_find_community`. There is no way to fix that through the interface. ## The API can already do this `PlatformChannelsController::destroy()` exists and calls `$platformChannel->delete()`. Only the Livewire UI has no path to it, so this is partly surfacing existing capability rather than building it. ## The cascade graph Enumerated by reading every migration, not just the ones the ticket originally listed: | Table | FK to `platform_channels` | Added | |---|---|---| | `platform_account_channels` | direct, cascade | `000003` | | `routes` | direct, cascade | `000004` | | `keywords` | direct, cascade | `000004` | | `route_articles` | composite `(feed_id, platform_channel_id)` → `routes`, cascade | `000007` | | `platform_channel_posts` | direct, cascade | `000013` | | **`article_publications`** | direct, cascade | **`000023` — added by this ticket** | **Five of the six already cascaded before this ticket started.** Only `article_publications` was missing a constraint — its `platform_channel_id` was a bare `unsignedBigInteger`. ## Decision: CASCADE Orphaned publications would not actually have broken anything — `PublicationsPerChannel` iterates channels and looks up counts by id rather than joining, `PublishSuccessRate` never touches channels, and `ArticlePublication` has no `platformChannel()` relation. So this was a **data-retention choice, not a breakage fix**. Chose `ON DELETE CASCADE`: the rows are unreachable once the channel is gone, so keeping them means carrying data nothing can read while the dashboard's totals silently disagree with a raw `COUNT(*)`. **Accepted consequence:** deleting a channel removes its `route_articles` and `article_publications`, so `ApprovalRate`, `PublishSuccessRate` and `ArticlesTrend` lose those data points for **past** days — a previously-rendered chart will change. No stat crashes. ## Acceptance criteria - [x] Channel cards show a delete action - [x] Deleting requires confirmation, and the confirmation states what will be removed with per-channel counts - [x] Cascade behaviour for `article_publications` decided, implemented, and documented in `PLATFORM.md` - [x] `platform_account_channels` links are cleaned up rather than orphaned — **already true before this ticket** (`000003`) - [x] Deleting a channel does not break the dashboard's per-channel breakdown or the publish-success-rate stat - [x] Feature tests cover the delete path, the confirmation, and the full cascade ## Notes - Related: #137 "Channel cards have no edit option" made the community pairing immutable, which is what makes delete the re-point mechanism. - The equivalent gap likely exists for feeds — `Feeds.php` has no delete path either. Worth a separate ticket rather than widening this one.
myrmidex added this to the v1.4.0 milestone 2026-08-13 21:12:55 +02:00
myrmidex added the
enhancement
label 2026-08-13 21:12:55 +02:00
Author
Owner

Delivered in two commits.

6270c97  Add a cascading foreign key to article publications
2b87f1f  Add channel deletion to the Channels page

Split so the schema change stands alone and is bisect-safe before any UI can trigger it.

What shipped

  • 2024_01_01_000023ON DELETE CASCADE FK on article_publications.platform_channel_id, plus an orphan cleanup that logs a warning with the row count before deleting (dev had zero; production unknown at the time).
  • Channels::deleteChannel()find() + early return, so a replayed request for an already-deleted channel no-ops rather than 500ing. Clears managingChannelId/editingChannelId if the deleted channel was open in either modal, and writes an ActionPerformed audit entry.
  • A delete button per card with wire:confirm stating that channel's own article and publication counts, sourced from a deletionImpact() helper.

The delete does nothing but $channel->delete() — the database does the rest.

Two things verified by breaking them

Rather than trusting the tests, I confirmed each is load-bearing:

  1. Removing the FK from the migration makes ChannelDeletionCascadeTest fail on the ArticlePublication assertion — so the cascade is genuinely database-enforced, not Eloquent.
  2. Changing deletionImpact() to return global totals makes the two-channel test fail: both cards render "4 articles" instead of "3" and "1". So the per-channel scoping is really pinned.

The FK is confirmed live in dev — information_schema.REFERENTIAL_CONSTRAINTS reports article_publications_platform_channel_id_foreign with DELETE_RULE = CASCADE.

Notes from the finish-phase review

The full-diff pr-reviewer enumerated the cascade graph independently by reading every migration and found no table left to orphan or to block the delete. It also surfaced something neither of us had named: the new FK improves a pre-existing race. If a channel is deleted while a publish job holds an already-hydrated RouteArticle, ArticlePublication::create() now fails the constraint and is caught as a publish failure, rather than inserting an orphan.

Verification

1231 tests / 2952 assertions green, Pint clean (352 files), PHPStan clean. Per-commit code-reviewer on both plus a finish-phase pr-reviewer over the combined diff — no critical, must-fix or should-fix issues at any point.

Not verified in a browser. The button, confirmation and cascade all pass in tests and the FK is live in dev, but the UI flow has not been exercised by hand. Worth doing before relying on it, since the action is irreversible and now genuinely removes publication history.

Follow-up

Feeds.php still has no delete path — the same gap this ticket closed for channels. Not filed.

Delivered in two commits. ``` 6270c97 Add a cascading foreign key to article publications 2b87f1f Add channel deletion to the Channels page ``` Split so the schema change stands alone and is bisect-safe before any UI can trigger it. ## What shipped - **`2024_01_01_000023`** — `ON DELETE CASCADE` FK on `article_publications.platform_channel_id`, plus an orphan cleanup that logs a warning with the row count before deleting (dev had zero; production unknown at the time). - **`Channels::deleteChannel()`** — `find()` + early return, so a replayed request for an already-deleted channel no-ops rather than 500ing. Clears `managingChannelId`/`editingChannelId` if the deleted channel was open in either modal, and writes an `ActionPerformed` audit entry. - **A delete button per card** with `wire:confirm` stating that channel's own article and publication counts, sourced from a `deletionImpact()` helper. The delete does nothing but `$channel->delete()` — the database does the rest. ## Two things verified by breaking them Rather than trusting the tests, I confirmed each is load-bearing: 1. **Removing the FK** from the migration makes `ChannelDeletionCascadeTest` fail on the `ArticlePublication` assertion — so the cascade is genuinely database-enforced, not Eloquent. 2. **Changing `deletionImpact()` to return global totals** makes the two-channel test fail: both cards render "4 articles" instead of "3" and "1". So the per-channel scoping is really pinned. The FK is confirmed live in dev — `information_schema.REFERENTIAL_CONSTRAINTS` reports `article_publications_platform_channel_id_foreign` with `DELETE_RULE = CASCADE`. ## Notes from the finish-phase review The full-diff `pr-reviewer` enumerated the cascade graph independently by reading every migration and found no table left to orphan or to block the delete. It also surfaced something neither of us had named: **the new FK improves a pre-existing race.** If a channel is deleted while a publish job holds an already-hydrated `RouteArticle`, `ArticlePublication::create()` now fails the constraint and is caught as a publish failure, rather than inserting an orphan. ## Verification 1231 tests / 2952 assertions green, Pint clean (352 files), PHPStan clean. Per-commit `code-reviewer` on both plus a finish-phase `pr-reviewer` over the combined diff — no critical, must-fix or should-fix issues at any point. **Not verified in a browser.** The button, confirmation and cascade all pass in tests and the FK is live in dev, but the UI flow has not been exercised by hand. Worth doing before relying on it, since the action is irreversible and now genuinely removes publication history. ## Follow-up `Feeds.php` still has no delete path — the same gap this ticket closed for channels. Not filed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#141
No description provided.