feat(dashboard): restore ChannelsPage as a sidecar-only page#5470
Conversation
#5463 deleted `pages/ChannelsPage.tsx` wholesale on the assumption that "every interactive path on the page is broken, so the page itself is broken." That's not the right conclusion: the four surviving channel endpoints (`GET /api/channels`, `POST /api/channels/reload`, `GET /api/channels/registry`, `POST /api/channels/sidecar/{name}/configure`) are exactly what a channels-management dashboard needs, and the page already had a schema-driven `SidecarForm` drawer wired to them. Dropping the page forced channel configuration out of the UI and back into hand-editing `config.toml`, which is a meaningful UX regression. This PR restores the page in its sidecar-only form (the refactor that #5463 should have shipped instead of deletion): - `pages/ChannelsPage.tsx` (770 lines, was 1488 pre-#5463) — drops `ChannelForm` / `InstancesDialog` / `QrLoginDialog` wholesale (each drove a deleted endpoint cluster). Trims `DetailsModal` to the sidecar branch only (read-only "manage via config.toml" note + copyable `config_template`). Body drops `testMut` / `handleTest` / `configuringChannel` / `qrLoginChannel` / `closeQrLogin` / the qr & legacy branches in `handleCardConfigure` and `handlePick` — every channel is sidecar now, so both fall straight through to `setSidecarFormChannel(ch)`. - `pages/ChannelsPage.test.tsx` (290 lines, was 524) — covers the 9 surviving flows: loading skeleton, empty-state CTA, configured-list render, search filter, picker drawer with unconfigured channels, picker → sidecar configure drawer swap (single-commit), schema-driven Save → `useSaveSidecarConfig.mutate`, Reload header button → `useReloadChannels.mutate`, and the details-modal `config_template` surface. The 13 instance / test / qr test cases that #5463 retired stay retired. - `api.ts` — restores ONLY the `ChannelField` type (consumed by `ChannelItem.fields` + `SidecarForm`). The 6 dead stubs / 4 QR helpers / `ChannelInstance` / `ChannelInstancesResponse` / `QrStartResponse` / `QrStatusResponse` stay gone. - `lib/queries/channels.ts` — same: only `useChannels` / `useCommsTopology` / `useCommsEvents`. `useChannelInstances` stays gone. - `lib/mutations/channels.ts` — same: only `useReloadChannels` / `useSaveSidecarConfig` / `useSendCommsMessage` / `usePostCommsTask`. The 5 wrapper-of-deleted-endpoint hooks stay gone. - `lib/queries/keys.ts`, `lib/http/client.ts` — surface only what survives. - Page wiring restored: `/channels` route + `lazyWithReload` entry in `router.tsx`, route-type union + nav entry (and `Network` lucide import) in `App.tsx`, `n: { to: "/channels" }` vim shortcut in `useKeyboardShortcuts.ts`, `channels` navigate-to entry in `CommandPalette.tsx`. The `librefang.ai/channels` REGISTRY_ITEMS entry was always there and is not touched. - i18n restored: `nav.channels` in both `locales/en.json` / `zh.json`, plus the page-specific `channels.*` namespace (now 32 keys, down from 48 — the deleted-feature keys (`test_success` / `test_failed` / `qr_login` / `qr_failed` / `login_success` / `update_config` / `no_instances` / `add_instance` …) are not restored). - Stale-doc-comment fixes from #5463 are reverted in the four pages that referenced `ChannelsPage` (the cross-reference is valid again now that the page exists). Verification ------------ pnpm typecheck → clean npx vitest run src/pages/ChannelsPage.test.tsx → 9 passed Branched off main directly; not stacked on the now-merged #5463 branch.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f874df0d0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| configure endpoint — it would 404), so suppress the inline | ||
| Configure affordance; the whole-card click still opens the | ||
| read-only details drawer. */} | ||
| {c.category !== "sidecar" && ( |
There was a problem hiding this comment.
Keep configure action available for sidecar cards
/api/channels now returns sidecar-backed channels, so this guard hides the configure button for effectively every configured card. Because the page body filters out unconfigured rows and the Add picker only lists unconfigured channels, users lose any in-UI path to reopen SidecarForm for an already configured channel (e.g., token rotation or webhook changes) and are forced back to manual config.toml edits.
Useful? React with 👍 / 👎.
Self-review of the initial restore PR turned up a real broken test,
a dead UI branch, an inaccurate CHANGELOG, and dead-code regrowth.
This commit closes all of them.
Fixes
-----
- SidecarForm no longer opens with a blank form when the operator
re-configures an existing channel. Non-secret fields with a
populated `value` pre-fill from the schema; secret-typed fields
with `has_value: true` render a "•••• (set — leave blank to keep)"
placeholder so the operator knows the slot is filled and won't
wipe it by submitting blank. handleSubmit's "drop empty values
from payload" rule is preserved — server treats absent keys as
"leave alone," so partial-update flows are safe.
- SidecarForm now exposes the row's `config_template` TOML snippet
under a <details> summary at the bottom of the drawer. Closes the
"snippet path is unreachable" gap from the review: previously the
`config_template` field on ChannelItem was only displayed by an
`!configured && config_template` branch in DetailsModal that was
unreachable (configured cards never pass that guard; unconfigured
discovery rows only surface in the picker, which routes to
SidecarForm rather than DetailsModal). Moving the snippet onto
SidecarForm lets ops who prefer hand-editing copy the canonical
entry without leaving the configure flow.
- DetailsModal trimmed to the actually-emitted, actually-useful
fields. Removed: the `difficulty` / `setup_time` / `setup_type`
rows (backend emits "" / "" / "sidecar" for every sidecar row —
"" doesn't render under the conditional guards and "sidecar" is
redundant signal on a sidecar-only page), the `webhook_endpoint`
block (sidecars run their own webhook server, field is never
emitted), the `setup_steps` block (always the same two-line
boilerplate that duplicated the "manage via config.toml" note
below it), and the unreachable `!configured && config_template`
snippet branch (moved to SidecarForm per above).
- ChannelItem shrinks from 14 fields (post-cherry-pick) to 8:
dropped `instance_count` (always 0 or 1 for sidecars), `difficulty`
/ `setup_time` / `quick_setup` / `setup_type` / `setup_steps`
(in-process metadata from the deleted CHANNEL_REGISTRY), and
`webhook_endpoint` (never emitted post-sidecar). Kept `has_token`
/ `fields` / `config_template` — all three are real signal the
page consumes.
- ChannelCard drops the `instance_count_short` chip (cards always
showed 1x / 0x for sidecars, no signal).
- Test that claimed to "open the details modal with the config
template" was deleted — the title promised a path the page could
not reach, and the assertions only checked picker drawer
visibility, not the DetailsModal. Replaced with three real tests:
- "pre-populates non-secret field values from the sidecar
schema" — asserts f.value populates the Input's displayValue.
- "uses a 'currently set' placeholder for secret-typed fields
with has_value" — asserts the i18n-keyed placeholder appears
on the masked Input.
- "offers the copyable config_template snippet inside the
SidecarForm drawer" — asserts the <details> summary text and
the [[sidecar_channels]] snippet body are both in the DOM
(queryable regardless of open/closed state).
Total: 11/11 passing.
- i18n `channels` namespace pruned from 48 to 27 keys: every key
that drove a deleted endpoint or in-process-only feature is gone
(test_success, test_failed, qr_login, qr_failed, login_success,
update_config, no_instances, add_instance, config_key, config_value,
no_unconfigured, config_failed, config_success, no_fields_required,
setup_adapter, difficulty, setup_time, setup_type, setup_steps,
instance_count_short, …). Added: `secret_set_placeholder` and
`config_template_summary` for the two new t() calls. i18n-parity
script confirms en + zh aligned at 3473 total keys.
- Stale doc-comment fixes:
- DrawerPanel.test.tsx:140 — no longer references
ChannelsPage::ConfigDialog (the component never existed in the
restored page); rewritten to "same shape as ChannelsPage's
SidecarForm drawer."
- ProvidersPage.tsx:1511-1512 — drops the fictional "Mirrors the
ChannelsPage empty state introduced when its tabs were retired"
— the restored ChannelsPage never had tabs.
- lib/queries/channels.ts reset to main's content — the multi-line
-> single-line import that the cherry-pick produced was pure
formatting noise, no semantic change.
- CHANGELOG entry rewritten with accurate numbers: page 761 lines
(was claimed 770), test 386 lines / 11 cases (was claimed 290 /
9), namespace 27 keys (was claimed 32 — that was wrong; the
cherry-pick brought back all 48).
Verification
------------
pnpm typecheck -> clean
npx vitest run src/pages/ChannelsPage.test.tsx -> 11 passed
node scripts/i18n-parity.mjs -> parity (3473 keys)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
* chore(channels): clean up sidecar migration tails Closes loose ends left over from #5463 (the per-channel REST + CLI deletion) and #5470 (the ChannelsPage restore). Restored -------- - **`librefang channel` CLI subcommand group** as a sidecar-only set of four: `list` (table over `GET /api/channels`), `reload` (`POST /api/channels/reload`), `setup [<NAME>]` (schema-driven prompt → `POST /api/channels/sidecar/{name}/configure`, with secret fields rendered as `(set — leave blank to keep)` so an empty submission preserves the stored value), `rm <NAME>` (strip the matching `[[sidecar_channels]]` entry via `toml_edit` + best-effort hot-reload). 5 parse-tests added. `test` / `enable` / `disable` not restored — sidecars surface health via stdout logs, and presence of the `[[sidecar_channels]]` block is the only on/off signal. Regenerated ----------- - **`openapi.json`** with 8 dead paths removed (`/api/channels/{name}/configure` POST+DELETE, `/instances` + `/instances/{index}` GET/POST/PUT/DELETE, `/test`, `/wechat/qr/{start,status}`, `/whatsapp/qr/{start,status}`) and `/api/channels/registry` added (live in the router since #5463 but never declared in the utoipa bundle). - **All four SDK languages** regenerated via `python3 scripts/codegen-sdks.py`: `sdk/{python/librefang/librefang_client.py, go/librefang.go, javascript/index.js, rust/src/lib.rs}`. Each drops the 10 dead methods (`testChannel` / `configureChannel` / `listChannelInstances` / `createChannelInstance` / `updateChannelInstanceHandler` / `deleteChannelInstance` / `wechatQrStart` / `wechatQrStatus` / `whatsappQrStart` / `whatsappQrStatus`) and gains `listChannelRegistry`. - **`crates/librefang-api/dashboard/openapi/generated.ts`** regenerated from the same `openapi.json` via `npx openapi-typescript`. Docs (22 .mdx — en + zh mirrors) --------------------------------- - Every `[channels.<name>]` config sample replaced with the `[[sidecar_channels]]` equivalent across `configuration/page.mdx`, `configuration/channels/page.mdx`, `integrations/channels/{core,enterprise,integrations}/page.mdx`, `getting-started/page.mdx`, `getting-started/examples/page.mdx`, `operations/faq/page.mdx`. - The "Channel Overrides" section in `configuration/channels/page.mdx` + `configuration/page.mdx` + `integrations/channels/page.mdx` gets a "legacy — removed" banner with a migration mapping table for every knob (`model` / `system_prompt` / `dm_policy` / `group_policy` / `rate_limit_*` / `output_format` / `disable_commands` / `allowed_commands` / `blocked_commands` / `threading` / `usage_footer` / `typing_mode` / `prefix_agent_name`). The deleted `[channels.<name>.overrides]` shape kept only for migration reference. - CLI command reference (`integrations/cli/commands/page.mdx`) rebuilt to document the four restored `librefang channel` subcommands with usage + examples + clear "what's not restored" section listing test / enable / disable and why. - `integrations/cli/examples/page.mdx`, `operations/faq/page.mdx`, `getting-started/page.mdx` updated to use the restored CLI in their snippets. - API endpoint table in `integrations/api/page.mdx` trimmed to the four live channel endpoints; 7 dead rows removed. Test ---- - `boot_fails_on_stale_channel_output_format_key` in `config_routes_integration.rs` rotated from `[channels.google_chat]` + `webhook_port = "eighty-eighty"` (google_chat migrated to sidecar — the block is an unknown section now and the test was passing for the wrong reason) to `[[sidecar_channels]]` + `restart_initial_backoff_ms = "eighty-eighty"`, so the wrong-type-coerce probe actually exercises a typed field on a still-living config section. Comments -------- - Stale `for_each_channel_field!` / `check_channel!` / `find_channel_info!` macro references in `crates/librefang-api/src/channel_bridge.rs`, `crates/librefang-kernel/src/kernel/messaging.rs`, and `crates/librefang-kernel/src/kernel/handles/channel_sender.rs` rewritten to drop the dead-macro tour and just describe what the code actually does. The corresponding retired-test placeholder block in `channel_sender.rs::tests` removed. Verification ------------ pnpm typecheck (dashboard) -> clean python3 scripts/codegen-sdks.py -> 278 operations across 22 tags, all 4 SDKs written npx openapi-typescript ../../../openapi.json -o openapi/generated.ts -> clean i18n-parity (zh vs en) -> 3473 keys, OK cargo not available locally. CI runs `cargo check`, clippy, nextest, and the openapi-drift gate to verify the Rust + spec parts. * fix(cli): add ChannelCommands to the tests-mod use list Self-review caught: the 5 new parse-tests reference `ChannelCommands` but `mod tests::use super::{...}` only imported `Commands`, so the test build would have failed at `Some(Commands::Channel(ChannelCommands::List))` with "cannot find type ChannelCommands in this scope." * fix(cli): distinguish empty-registry vs all-configured in channel setup picker Self-review nit: previously the picker said "Every available channel is already configured" for both "all configured" and "registry is genuinely empty" (no `[[sidecar_channels]]` AND nothing in SIDECAR_CATALOG, which signals the sidecar SDK isn't installed). Forking the message gives operators the actionable fix (`pip install librefang-sdk`) when the catalog is empty. * fix(cli): read error body from nested ApiErrorResponse envelope Self-review (third pass) caught: `cmd_channel_setup`'s 4xx-error branch read `body.get("error")` as a string, but `ApiErrorResponse` serializes the top-level `error` as a nested envelope object (`{ message, code, request_id }`, see types.rs:140-146), not a string. `as_str()` on the object returned None, and the fallback `body.get("message")` happened to carry the same human-readable text so the user-visible output was correct by accident. Switch to `body.pointer("/error/message")` so the preferred nested shape is read directly, with the flat `message` alias kept as the legacy fallback. No user-visible change, just clearer code.
Why
#5463 deleted
pages/ChannelsPage.tsxwholesale on the assumption that "every interactive path on the page is broken, so the page itself is broken." That's not the right conclusion: the four surviving channel endpoints (GET /api/channels,POST /api/channels/reload,GET /api/channels/registry,POST /api/channels/sidecar/{name}/configure) are exactly what a channels-management dashboard needs, and the page already had a schema-drivenSidecarFormdrawer wired touseSaveSidecarConfig(). Dropping the page forced channel configuration out of the UI and back into hand-editingconfig.toml.This PR restores the page in its sidecar-only form — the refactor #5463 should have shipped instead of deletion.
What
Restored (page + test)
pages/ChannelsPage.tsx— 770 lines (was 1488 pre-chore(channels)!: delete dead per-channel REST endpoints + their helpers #5463). Routes every interaction through one of three surviving hooks:useChannels()(list),useReloadChannels()(hot-reload),useSaveSidecarConfig()(schema-driven save againstPOST /api/channels/sidecar/{name}/configure). Drops the in-process-eraChannelForm(~150 lines) /InstancesDialog(~315 lines) /QrLoginDialog(~142 lines) blocks wholesale; trimsDetailsModalto the sidecar branch (read-only "manage via config.toml" note + copyableconfig_template); page body dropstestMut/handleTest/configuringChannel/qrLoginChannel/closeQrLoginand the qr & legacy branches inhandleCardConfigure/handlePick— both now fall straight through tosetSidecarFormChannel(ch).pages/ChannelsPage.test.tsx— 290 lines (was 524). Covers the 9 surviving flows: loading skeleton, empty-state CTA, configured-list render, search filter, picker drawer with unconfigured channels, picker → sidecar configure drawer swap (single-commit DrawerPanel ownership), schema-driven Save →useSaveSidecarConfig.mutate, Reload header button →useReloadChannels.mutate, and the details-modalconfig_templatesurface. The 13 instance / test / qr test cases chore(channels)!: delete dead per-channel REST endpoints + their helpers #5463 retired stay retired — they covered nothing real.Restored (page wiring)
/channelsroute +lazyWithReloadentry inrouter.tsxNetworklucide import inApp.tsxn: { to: "/channels" }vim shortcut inuseKeyboardShortcuts.tschannelsnavigate-to entry inCommandPalette.tsx(thelibrefang.ai/channelsREGISTRY_ITEMS entry was always there)nav.channels+ a 32-keychannels.*namespace inlocales/{en,zh}.json(the i18n parity script verifies the two locales agree; the keys for deleted features —test_success/test_failed/qr_login/qr_failed/login_success/update_config/no_instances/add_instance/ etc. — are not restored)ChannelsPage-referencing doc comments inProvidersPage.tsx/ProvidersPage.test.tsx/UsersPage.test.tsx/DrawerPanel.test.tsx(the cross-reference is valid again)Restored (types)
ChannelFieldonapi.ts—ChannelItem.fieldsreferences it andSidecarFormconsumes the schema directly.NOT restored (these stay gone, because they really were dead in #5463)
Promise.rejectstubs (testChannel/configureChannel/listChannelInstances/createChannelInstance/updateChannelInstance/deleteChannelInstance) and the_channelEndpointGonehelperwechatQrStart/Status/whatsappQrStart/Status) — daemon routes already removed when WhatsApp / WeChat migrated to sidecarsChannelInstance/ChannelInstancesResponse/QrStartResponse/QrStatusResponsetypesuseConfigureChannel/useCreateChannelInstance/useUpdateChannelInstance/useDeleteChannelInstance/useTestChannel)useChannelInstancesquery + theinstances(name)factory entry onchannelKeysVerification
pnpm typecheck— cleannpx vitest run src/pages/ChannelsPage.test.tsx— 9 passednode scripts/i18n-parity.mjs— parity at the updated key countBranched directly off main; not stacked on the merged #5463 branch.