Skip to content

Add a retry action for partial publications, and surface real API errors - #14

Merged
sigmanor merged 6 commits into
mainfrom
feat/retry-failed-publication
Aug 17, 2026
Merged

Add a retry action for partial publications, and surface real API errors#14
sigmanor merged 6 commits into
mainfrom
feat/retry-failed-publication

Conversation

@sigmanor

Copy link
Copy Markdown
Member

Why

When a message run reaches some integrations but not others, the repository is already marked as posted, so it drops out of the publication queue and the integrations that failed never receive it. The dashboard showed the partial run and offered nothing to do about it.

At the same time every Content Alchemist failure was reported as Failed to connect to Content Alchemist API. That wording hid a real rejection: the service was answering 400 because it could not validate a language code (see think-root/content-alchemist#6), which reads as a service outage rather than a rejected request.

What changed

Three commits:

  1. fix(language) — the ISO 639-1 list is bundled instead of fetched from a raw gist that GitHub rate-limits. The old code silently degraded to a format-only check whenever that fetch failed, so xx passed validation. Needs resolveJsonModule in tsconfig.app.json.

  2. fix(errors) — the thrown message is surfaced, prefixed with the service name; the generic wording remains only for errors that carry no message.

  3. feat(cron) — failed and partial message runs get a retry action in the Cron History (desktop table and mobile cards), backed by the new POST /api/message/retry in Content Maestro (Add a manual retry for partial publications, and stop mislabelling rejections as an empty queue content-maestro#2).

Behaviour of the retry, and the reasoning worth reviewing:

  • It appears only where it can work: message runs that are not fully successful and whose failed integrations are known. Successful runs and collect runs show nothing.
  • The integrations come from details.failed on the history record. Runs recorded before Content Maestro tracked details fall back to parsing Failed: … out of the output — that parser targets the legacy wording only, since anything newer carries the structured field.
  • Those legacy runs also carry no url, so Content Maestro resolves the latest published repository. The confirm dialog fetches and names that repository first, because a cron run in between would have moved the target and the user should see what they are about to publish.
  • A retry can take half a minute (one production publish took ~28s), so it uses a loading toast that is replaced in place by the result. Failures show the per-integration reasons the endpoint returns, not a generic message.
  • The history is refreshed afterwards through a new refresh on useCronJobHistory, which re-reads the current page with the active filters.

Testing

  • npx tsc -b reports no new errors, npm run build is clean.
  • Verified in the browser against a real Content Maestro binary with stubbed backends: the button appears on failed rows only, the dialog names the repository (Send https://github.com/resemble-ai/chatterbox to threads?), confirming publishes and swaps the loading toast for Sent to threads, and the new manual run appears at the top of the history without a manual reload. Against a Content Alchemist that rejects requests, the toast reports the real rejection reason.

Notes

  • Two pre-existing TS2367 errors in useRepositories.ts are unrelated to this branch and left alone.
  • eslint currently cannot run in this repo (no-unused-expressions rule fails to load, a plugin/version conflict) and there is no lint script, so type-check and build were used as the gate.

The validator fetched the ISO 639-1 list from a raw gist that GitHub now
rate-limits, so validation silently degraded to a format-only check on every
input. The list is a static standard, so it ships with the app instead.

Enables resolveJsonModule so the list can be imported directly.
Every failed call was reported as "Failed to connect to Content Alchemist API",
whatever went wrong. That hid a real rejection - content-alchemist answering 400
because it could not validate a language code - behind a connection error, and a
rejected request needs a different fix than an unreachable service.

The thrown message is now surfaced, prefixed with the service name, and the
generic wording is kept only for errors that carry no message.
A message run marks its repository as posted as soon as one integration
succeeds, so the integrations that failed can never recover the item on a later
run and the dashboard offered no way to finish the job by hand.

Failed and partial message runs now carry a retry action that re-sends the item
to the integrations named in the run details, with a confirm dialog, a loading
toast that resolves into the per-integration result, and a history refresh
afterwards. Runs recorded before Content Maestro tracked those details fall back
to parsing their output, and the dialog then names the repository that would be
used so an intervening cron run cannot come as a surprise.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying content-sentinel with  Cloudflare Pages  Cloudflare Pages

Latest commit: 75a099b
Status: ✅  Deploy successful!
Preview URL: https://e176f3b5.content-sentinel.pages.dev
Branch Preview URL: https://feat-retry-failed-publicatio.content-sentinel.pages.dev

View logs

@sigmanor sigmanor self-assigned this Aug 17, 2026
@sigmanor sigmanor added bug Something isn't working enhancement New feature or request labels Aug 17, 2026
The retry action appeared on any message run that was not fully successful, but
a run can fail without ever resolving an item: an empty queue, or Content
Alchemist rejecting every request, makes the job report every integration as
failed while recording no repository. Retrying such a run fell back to "the
latest published repository" and re-published it to every connector - a public,
irreversible duplicate.

A run is now retryable only when both halves are known: the integrations that
missed the item and the item itself. Legacy runs recorded before the details
field are still covered - the older output wording carries the repository, and
where it does not the url is resolved and pinned into the request rather than
left to the backend, so the confirm dialog names exactly what will be published.

Also in this path:
- the legacy parser no longer stops at the first period, so integration names
  containing one survive, the oldest output format is recognised, and "Failed:"
  appearing inside an error message is no longer read as a list of integrations.
- the history refresh moved out of the publish try block: its failure used to
  overwrite the success toast with a fetch error, inviting a second click.
- a row whose retry succeeded stops offering the action, since history rows are
  immutable and keep looking unhandled.
- refresh reads the live filter state through a ref, so a slow retry cannot drag
  the user back to the page they started on.
- the loading skeleton has the same column count as the table it stands in for.
A rejected fetch surfaces as "TypeError: Failed to fetch", which said less than
the generic message it replaced. That case now keeps the plain "failed to
connect" wording; messages the API actually sent are still shown.
Adds vitest and a table covering what may and may not be retried: runs with
structured details, runs that resolved no item, successful and non-message runs,
and the three output formats Content Maestro has used over time.

This parser decides whether a button can publish to production, and every bug
found in review was in exactly these branches.
@sigmanor
sigmanor merged commit 792efcd into main Aug 17, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant