Skip to content

Make web message sending idempotent and improve delivery reliability - #15

Merged
frankwei98 merged 16 commits into
mainfrom
codex/fix-web-send-idempotency
Jul 29, 2026
Merged

frankwei98 merged 16 commits into
mainfrom
codex/fix-web-send-idempotency

Conversation

@frankwei98

@frankwei98 frankwei98 commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add idempotency handling for web-triggered message sends.
  • Refine delivery, forwarding, persistence, monitoring, configuration, and runtime flows.
  • Improve the frontend message console, configuration editors, authentication, localization, and API/event handling.
  • Update installation, operations documentation, and related dependency lockfiles.

Testing

  • Added and updated Rust and frontend tests covering API behavior, events, monitoring, configuration editing, authentication, localization, and message-console interactions.
  • Not run (not requested).

Summary by CodeRabbit

  • New Features
    • Added configurable webhook forwarding (GET/POST) with URL/body templates, static headers, validation, and HTTP status/retry/redirect handling.
    • Added trusted_proxies for proxy-aware client handling.
    • Added runtime monitoring toggle with /api/monitoring and privacy-conscious setup prompts.
    • Added message send idempotency, localized timestamps, and safer CSV/JSON exports.
  • Bug Fixes
    • Improved retry and error flows for message load/send and refreshable failures.
    • Hardened delivery timing and secure SQLite opening/permissions.
  • Breaking Changes
    • Shell forwarding has been removed; existing configurations must migrate to webhooks.
  • Documentation
    • Updated EN/CN docs for webhook forwarding, monitoring, and configuration guidance.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80353441-1220-4486-b620-6e1e73a260d5

📥 Commits

Reviewing files that changed from the base of the PR and between 55656bc and 9fa64f6.

📒 Files selected for processing (9)
  • frontend/src/channel-editor.test.tsx
  • frontend/src/components/config/channel-editor.tsx
  • frontend/src/components/messages/message-console.tsx
  • frontend/src/components/messages/message-filters.tsx
  • frontend/src/locales/es.ts
  • frontend/src/locales/ja.ts
  • frontend/src/main.tsx
  • frontend/src/message-console.test.tsx
  • src/forward/webhook.rs
💤 Files with no reviewable changes (1)
  • frontend/src/components/messages/message-filters.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • frontend/src/locales/es.ts
  • src/forward/webhook.rs
  • frontend/src/main.tsx
  • frontend/src/channel-editor.test.tsx
  • frontend/src/message-console.test.tsx
  • frontend/src/locales/ja.ts
  • frontend/src/components/config/channel-editor.tsx
  • frontend/src/components/messages/message-console.tsx

📝 Walkthrough

Walkthrough

The pull request replaces shell forwarding with validated HTTP webhooks, adds trusted-proxy and monitoring configuration, improves authentication and message-operation recovery, localizes timestamps, hardens SQLite and systemd installation behavior, and updates related documentation, tests, and translations.

Changes

Webhook forwarding and configuration

Layer / File(s) Summary
Webhook configuration and editor
src/config.rs, src/wizard.rs, frontend/src/components/config/*, frontend/src/lib/config-model.ts
Adds webhook profiles, templates, headers, validation, GET warnings, migration errors, and removes shell timeout settings.
Webhook forwarding pipeline
src/forward/webhook.rs, src/delivery/*, src/runner.rs, src/runtime.rs
Renders webhook requests, classifies HTTP outcomes, disables redirects, wires delivery retries, and records retry timing from dispatch completion.
Documentation and localized configuration UI
README.md, docs/operations/*, frontend/src/locales/*
Documents webhook semantics, monitoring and proxy settings, migration rules, and translated configuration labels.

Authentication and monitoring

Layer / File(s) Summary
Trusted proxy handling
src/api/auth.rs, src/api/config.rs, src/api/mod.rs, frontend/src/lib/config-api.ts, frontend/src/components/config/*
Derives client IPs from trusted proxy chains and reports trusted-proxy changes during configuration preview.
Monitoring preference
src/monitoring.rs, src/api/mod.rs, frontend/src/lib/monitoring.ts, frontend/src/main.tsx, src/main.rs
Loads monitoring state from configuration or /api/monitoring before initializing Sentry.
Unauthorized events and logout
frontend/src/lib/api.ts, frontend/src/lib/events.ts, frontend/src/routes/__root.tsx
Broadcasts unauthorized responses, invalidates local authentication, and adds logout handling.

Messaging and runtime hardening

Layer / File(s) Summary
Message operations and downloads
frontend/src/components/messages/*, frontend/src/lib/api.ts, frontend/src/message-console.test.tsx
Adds idempotent sends, retryable loading, operation error banners, localized timestamps, and authenticated downloads.
Storage, modem, and installation hardening
src/storage/mod.rs, src/persistence/mod.rs, src/modem.rs, install.sh, tests/install.sh
Restricts SQLite files and sidecars, requires verified modem paths, and verifies binary and systemd service hardening.
Frontend workspace configuration
frontend/package.json, frontend/pnpm-workspace.yaml
Moves pnpm build settings and dependency overrides into workspace configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeliveryWorker
  participant ProductionDispatcher
  participant Webhook
  participant WebhookEndpoint
  DeliveryWorker->>ProductionDispatcher: dispatch webhook profile
  ProductionDispatcher->>Webhook: send WebhookMessage
  Webhook->>WebhookEndpoint: GET or POST rendered request
  WebhookEndpoint-->>Webhook: HTTP status or transport error
  Webhook-->>ProductionDispatcher: success, transient, or permanent outcome
  ProductionDispatcher-->>DeliveryWorker: delivery result and retry timing
Loading
sequenceDiagram
  participant Frontend
  participant API
  participant AuthRoot
  Frontend->>API: request protected resource
  API-->>Frontend: 401 response
  Frontend->>AuthRoot: dispatch unauthorized event
  AuthRoot->>AuthRoot: clear authenticated state
  AuthRoot-->>Frontend: navigate to login
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main idempotency and delivery-reliability focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-web-send-idempotency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/messages/message-console.tsx (1)

428-441: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark-read errors mislabeled when the follow-up reload fails.

Both handleMarkConversationRead (556-571) and the auto mark-read effect (428-441) wrap the mark POST and reloadActiveViews() in one try/catch, always setting operationError("markRead"). If marking succeeds but the reload fails, the banner still says "could not mark as read," which is misleading. handleMarkSelected/handleDeleteSelected already split these into separate try/catch blocks with distinct error codes (update/delete vs refresh) — worth applying the same split here for consistency.

♻️ Proposed fix
 	async function handleMarkConversationRead() {
 		if (!selectedPhone) return;
 		setOperationError(null);
 		try {
 			await apiFetch(
 				`/api/conversations/${encodeURIComponent(selectedPhone)}/read`,
 				{
 					method: "POST",
 				},
 			);
-			await reloadActiveViews();
 		} catch (err) {
 			setOperationError("markRead");
+			console.error(err);
+			return;
+		}
+		try {
+			await reloadActiveViews();
+		} catch (err) {
+			setOperationError("refresh");
 			console.error(err);
 		}
 	}

Also applies to: 556-571

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/messages/message-console.tsx` around lines 428 - 441,
Separate the mark-read POST and follow-up reload error handling in both the auto
mark-read effect and handleMarkConversationRead. Keep mark-read failures mapped
to operationError("markRead"), but catch reloadActiveViews failures separately
and map them to operationError("refresh"), preserving cleanup of
markingReadPhonesRef.
frontend/src/locales/ja.ts (1)

512-513: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale shell wording left in the Japanese timeouts description.

config.fields.timeouts.sectionDescription still mentions シェルプロファイルの実行, but shell forwarding was removed in this PR. The equivalent string was updated in es.ts (Line 524), fr.ts (Line 522), ko.ts (Line 507) and zh-CN.ts (Line 482); ja.ts was missed.

🌐 Proposed fix
 				sectionDescription:
-					"接続の確立、プロバイダーリクエスト、およびシェルプロファイルの実行を制限します。すべての値は秒単位です。",
+					"接続の確立と、プロバイダーまたは Webhook へのリクエストを制限します。すべての値は秒単位です。",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/locales/ja.ts` around lines 512 - 513, Update
config.fields.timeouts.sectionDescription in the Japanese locale to remove the
outdated シェルプロファイルの実行 wording, keeping only the description of connection
establishment and provider requests while preserving the existing seconds-unit
wording.
🧹 Nitpick comments (1)
frontend/src/components/config/channel-editor.tsx (1)

605-669: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Link the header-name error to the input via aria-describedby.

aria-invalid is set but the error paragraph isn't associated with the input via id/aria-describedby, unlike the duplicate-profile-name pattern used elsewhere in this file (lines 439–457). Screen reader users get "invalid" without the reason.

♻️ Proposed fix
 function WebhookHeaderRow({
 	name,
 	value,
 	allNames,
 	onChange,
 }: {
 	name: string;
 	value: string;
 	allNames: string[];
 	onChange: (oldName: string, name: string, value: string) => void;
 }) {
 	const { t } = useTranslation();
 	const [draftName, setDraftName] = useState(name);
 	const missing = draftName.length === 0;
 	const duplicate =
 		draftName.toLowerCase() !== name.toLowerCase() &&
 		allNames.some(
 			(existingName) => existingName.toLowerCase() === draftName.toLowerCase(),
 		);
 	const invalid = missing || duplicate;
+	const errorId = `webhook-header-${name || "new"}-error`;

 	return (
 		<div>
 			<div className="grid grid-cols-[1fr_1fr_auto] gap-2">
 				<Input
 					aria-label={t("config.channel.webhookHeaderName")}
 					aria-invalid={invalid}
+					aria-describedby={invalid ? errorId : undefined}
 					value={draftName}
 					onChange={(event) => setDraftName(event.target.value)}
 					onBlur={() => {
 						if (!invalid && draftName !== name) {
 							onChange(name, draftName, value);
 						}
 					}}
 					className="h-8 font-mono text-xs"
 				/>
 				...
 			</div>
 			{invalid ? (
-				<p className="mt-1 text-xs text-destructive">
+				<p id={errorId} className="mt-1 text-xs text-destructive">
 					{t(
 						duplicate
 							? "config.channel.webhookHeaderDuplicate"
 							: "config.channel.webhookHeaderRequired",
 					)}
 				</p>
 			) : null}
 		</div>
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/config/channel-editor.tsx` around lines 605 - 669,
Associate the header-name validation message in WebhookHeaderRow with its name
Input by assigning a stable unique id and setting aria-describedby to that id
when invalid. Add the matching id to the conditional error paragraph, following
the existing duplicate-profile-name pattern, while preserving the current
aria-invalid and validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/messages/message-filters.tsx`:
- Line 4: Update the CSV and JSON export handlers in MessageFilters to attach
rejection handling to each void downloadFile call, matching the existing
message-console.tsx/FilterDialog pattern and surfacing the export error through
the component’s established UI feedback mechanism.

In `@frontend/src/locales/es.ts`:
- Around line 324-334: Update the Spanish messages.error strings in the error
object and thread.sendFailed to use the formal usted register, replacing
informal “Inténtalo de nuevo” wording with “Inténtelo de nuevo” while preserving
the existing translations and message structure.

In `@frontend/src/main.tsx`:
- Around line 25-28: Move the rootElement.innerHTML guard in start() before
loadMonitoringPreference() and initMonitoring(), returning immediately on repeat
invocation so no monitoring fetch or re-initialization occurs when mounting is
skipped.

In `@src/config.rs`:
- Around line 705-714: Update config validation to match other channel types by
removing the unconditional webhook profile validation loop in validate(). Keep
webhook validation through profile_for_ref when a webhook is referenced by
forward.enabled, and update
validates_webhook_templates_and_redacts_url_and_headers to reflect that disabled
or unreferenced incomplete profiles are allowed.

In `@src/forward/webhook.rs`:
- Around line 61-70: Update validate_profile around render_template and URL
parsing to reject any {SENDER}, {MESSAGE}, or {DATETIME} token appearing in the
URL authority (host, port, or userinfo), preventing runtime-controlled
destinations. Require URL-encoded token forms when values are intended elsewhere
in the URL, and preserve the existing http/https scheme validation. Ensure
send() cannot receive a profile whose rendered authority can vary with inbound
SMS content.

In `@src/storage/mod.rs`:
- Around line 312-339: Add a Unix-only restrictive umask guard around the
connection opening, WAL setup, and Self::migrate() sequence in the storage
initialization flow, so SQLite-created sidecars begin with 0600 permissions.
Scope the guard tightly and ensure the original process umask is restored on
every exit path, including errors; keep existing restrict_sqlite_file and
restrict_sqlite_sidecars calls as defense-in-depth and leave non-Unix behavior
unchanged.

---

Outside diff comments:
In `@frontend/src/components/messages/message-console.tsx`:
- Around line 428-441: Separate the mark-read POST and follow-up reload error
handling in both the auto mark-read effect and handleMarkConversationRead. Keep
mark-read failures mapped to operationError("markRead"), but catch
reloadActiveViews failures separately and map them to operationError("refresh"),
preserving cleanup of markingReadPhonesRef.

In `@frontend/src/locales/ja.ts`:
- Around line 512-513: Update config.fields.timeouts.sectionDescription in the
Japanese locale to remove the outdated シェルプロファイルの実行 wording, keeping only the
description of connection establishment and provider requests while preserving
the existing seconds-unit wording.

---

Nitpick comments:
In `@frontend/src/components/config/channel-editor.tsx`:
- Around line 605-669: Associate the header-name validation message in
WebhookHeaderRow with its name Input by assigning a stable unique id and setting
aria-describedby to that id when invalid. Add the matching id to the conditional
error paragraph, following the existing duplicate-profile-name pattern, while
preserving the current aria-invalid and validation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d661975b-2dc6-4987-a985-714d3aeebe16

📥 Commits

Reviewing files that changed from the base of the PR and between f1595e9 and 55656bc.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (52)
  • README.md
  • docs/operations/long-running-validation.md
  • frontend/package.json
  • frontend/pnpm-workspace.yaml
  • frontend/src/channel-editor.test.tsx
  • frontend/src/components/config/channel-editor.tsx
  • frontend/src/components/config/config-editor.tsx
  • frontend/src/components/config/config-section-editors.tsx
  • frontend/src/components/config/config-sections.ts
  • frontend/src/components/messages/message-console.tsx
  • frontend/src/components/messages/message-filters.tsx
  • frontend/src/config-editor.test.tsx
  • frontend/src/language-switcher.test.tsx
  • frontend/src/lib/api.test.ts
  • frontend/src/lib/api.ts
  • frontend/src/lib/config-api.ts
  • frontend/src/lib/config-model.ts
  • frontend/src/lib/events.test.ts
  • frontend/src/lib/events.ts
  • frontend/src/lib/monitoring.test.ts
  • frontend/src/lib/monitoring.ts
  • frontend/src/locales/en.ts
  • frontend/src/locales/es.ts
  • frontend/src/locales/fr.ts
  • frontend/src/locales/ja.ts
  • frontend/src/locales/ko.ts
  • frontend/src/locales/zh-CN.ts
  • frontend/src/main.tsx
  • frontend/src/message-console.test.tsx
  • frontend/src/root-auth.test.tsx
  • frontend/src/routes/__root.tsx
  • install.sh
  • src/api/auth.rs
  • src/api/config.rs
  • src/api/mod.rs
  • src/api/modem.rs
  • src/config.rs
  • src/delivery.rs
  • src/delivery/dispatcher.rs
  • src/delivery/worker.rs
  • src/forward/mod.rs
  • src/forward/shell.rs
  • src/forward/webhook.rs
  • src/main.rs
  • src/modem.rs
  • src/monitoring.rs
  • src/persistence/mod.rs
  • src/runner.rs
  • src/runtime.rs
  • src/storage/mod.rs
  • src/wizard.rs
  • tests/install.sh
💤 Files with no reviewable changes (2)
  • frontend/src/components/config/config-section-editors.tsx
  • src/forward/shell.rs

Comment thread frontend/src/components/messages/message-filters.tsx Outdated
Comment thread frontend/src/locales/es.ts
Comment thread frontend/src/main.tsx
Comment thread src/config.rs
Comment thread src/forward/webhook.rs
Comment thread src/storage/mod.rs
@frankwei98
frankwei98 merged commit c93ff03 into main Jul 29, 2026
6 checks passed
@frankwei98
frankwei98 deleted the codex/fix-web-send-idempotency branch July 29, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant