Skip to content

feat(dashboard): restore ChannelsPage as a sidecar-only page#5470

Merged
houko merged 2 commits into
mainfrom
fix/restore-channels-page
May 21, 2026
Merged

feat(dashboard): restore ChannelsPage as a sidecar-only page#5470
houko merged 2 commits into
mainfrom
fix/restore-channels-page

Conversation

@houko

@houko houko commented May 21, 2026

Copy link
Copy Markdown
Contributor

Why

#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 useSaveSidecarConfig(). Dropping the page forced channel configuration out of the UI and back into hand-editing config.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.tsx770 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 against POST /api/channels/sidecar/{name}/configure). Drops the in-process-era ChannelForm (~150 lines) / InstancesDialog (~315 lines) / QrLoginDialog (~142 lines) blocks wholesale; trims DetailsModal to the sidecar branch (read-only "manage via config.toml" note + copyable config_template); page body drops testMut / handleTest / configuringChannel / qrLoginChannel / closeQrLogin and the qr & legacy branches in handleCardConfigure / handlePick — both now fall straight through to setSidecarFormChannel(ch).

  • pages/ChannelsPage.test.tsx290 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-modal config_template surface. 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)

  • /channels route + lazyWithReload entry in router.tsx
  • Route-type union + runtime-section nav entry + 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)
  • nav.channels + a 32-key channels.* namespace in locales/{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)
  • Stale ChannelsPage-referencing doc comments in ProvidersPage.tsx / ProvidersPage.test.tsx / UsersPage.test.tsx / DrawerPanel.test.tsx (the cross-reference is valid again)

Restored (types)

  • ChannelField on api.tsChannelItem.fields references it and SidecarForm consumes the schema directly.

NOT restored (these stay gone, because they really were dead in #5463)

  • The 6 Promise.reject stubs (testChannel / configureChannel / listChannelInstances / createChannelInstance / updateChannelInstance / deleteChannelInstance) and the _channelEndpointGone helper
  • The 4 dead QR helpers (wechatQrStart / Status / whatsappQrStart / Status) — daemon routes already removed when WhatsApp / WeChat migrated to sidecars
  • ChannelInstance / ChannelInstancesResponse / QrStartResponse / QrStatusResponse types
  • The 5 wrapper mutation hooks (useConfigureChannel / useCreateChannelInstance / useUpdateChannelInstance / useDeleteChannelInstance / useTestChannel)
  • The useChannelInstances query + the instances(name) factory entry on channelKeys
  • The typed-http-client re-exports of all of the above

Verification

  • pnpm typecheck — clean
  • npx vitest run src/pages/ChannelsPage.test.tsx9 passed
  • node scripts/i18n-parity.mjs — parity at the updated key count

Branched directly off main; not stacked on the merged #5463 branch.

#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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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" && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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)
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@houko
houko merged commit 208ca88 into main May 21, 2026
14 of 15 checks passed
@houko
houko deleted the fix/restore-channels-page branch May 21, 2026 08:39
@github-actions github-actions Bot added the area/docs Documentation and guides label May 21, 2026
houko added a commit that referenced this pull request May 21, 2026
* 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.
@github-actions github-actions Bot added size/XL 1000+ lines changed no-rust-required This task does not require Rust knowledge labels May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides no-rust-required This task does not require Rust knowledge size/XL 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant