Skip to content

Commit cf4b57a

Browse files
committed
fix: route slack settings through gateway
1 parent 386d6a2 commit cf4b57a

7 files changed

Lines changed: 48 additions & 23 deletions

File tree

docs/2026-04-25-react-management-ui-migration-plan.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
date: 2026-04-25
3-
author: Onur Solmaz <onur@textcortex.com>
3+
author: Onur Solmaz
44
title: React Management UI Migration Plan
55
tags: [spritz, ui, react, slack-gateway, settings, migration]
66
---
@@ -43,7 +43,12 @@ The existing chat view should change as little as possible.
4343

4444
The migration should add only one settings entry point to the existing chat UI,
4545
such as a settings button or link in the current header or navigation area.
46-
That entry point should navigate to the new `/settings/*` route group.
46+
That entry point should go through the gateway management URL
47+
(`/slack-gateway/slack/workspaces`, or the configured absolute gateway URL).
48+
Same-origin/proxied deployments will redirect into the new `/settings/*` route
49+
group. Cross-origin deployments without a same-origin gateway proxy will remain
50+
on the gateway fallback pages instead of opening a React page that cannot reach
51+
the gateway APIs.
4752

4853
The settings experience should be its own React view. It should not redesign
4954
the chat page, add a settings sidebar inside the chat surface, or make Slack

integrations/slack-gateway/backend_client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func TestUpdateManagedInstallationConfigPostsExpectedPayload(t *testing.T) {
225225
func TestManagedChannelRoutesDefaultMissingBooleansSafely(t *testing.T) {
226226
var connection backendManagedConnection
227227
if err := json.Unmarshal(
228-
[]byte(`{"id":"cc_1","routes":[{"externalChannelId":"D_default","externalChannelType":"im"}]}`),
228+
[]byte(`{"id":"chconn_default","routes":[{"externalChannelId":"D_default","externalChannelType":"im"}]}`),
229229
&connection,
230230
); err != nil {
231231
t.Fatalf("decode connection: %v", err)

integrations/slack-gateway/gateway_test.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ func TestChannelSettingsRendersManagedConnections(t *testing.T) {
724724
"status": "resolved",
725725
"installations": []map[string]any{
726726
{
727-
"id": "ci_1",
727+
"id": "chinst_workspace_1",
728728
"route": map[string]any{
729729
"principalId": "shared-slack-gateway",
730730
"provider": "slack",
@@ -740,24 +740,24 @@ func TestChannelSettingsRendersManagedConnections(t *testing.T) {
740740
},
741741
"connections": []map[string]any{
742742
{
743-
"id": "cc_1",
743+
"id": "chconn_default",
744744
"isDefault": true,
745745
"state": "ready",
746746
"routes": []map[string]any{
747747
{
748-
"id": "cr_1",
748+
"id": "chroute_channel_1",
749749
"externalChannelId": "C_channel_1",
750750
"requireMention": false,
751751
"enabled": true,
752752
},
753753
},
754754
},
755755
{
756-
"id": "cc_2",
756+
"id": "chconn_secondary",
757757
"state": "ready",
758758
"routes": []map[string]any{
759759
{
760-
"id": "cr_2",
760+
"id": "chroute_channel_2",
761761
"externalChannelId": "C_channel_2",
762762
"requireMention": true,
763763
"enabled": true,
@@ -781,27 +781,27 @@ func TestChannelSettingsRendersManagedConnections(t *testing.T) {
781781
HTTPTimeout: 5 * time.Second,
782782
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
783783

784-
installationReq := httptest.NewRequest(http.MethodGet, "/settings/channels/installations/ci_1", nil)
784+
installationReq := httptest.NewRequest(http.MethodGet, "/settings/channels/installations/chinst_workspace_1", nil)
785785
installationReq.Header.Set("X-Spritz-User-Id", "user-1")
786786
installationRec := httptest.NewRecorder()
787787
gateway.routes().ServeHTTP(installationRec, installationReq)
788788

789789
if installationRec.Code != http.StatusSeeOther {
790790
t.Fatalf("expected installation redirect, got %d: %s", installationRec.Code, installationRec.Body.String())
791791
}
792-
if location := installationRec.Header().Get("Location"); location != "https://gateway.example.test/settings/slack/channels/installations/ci_1" {
792+
if location := installationRec.Header().Get("Location"); location != "https://gateway.example.test/settings/slack/channels/installations/chinst_workspace_1" {
793793
t.Fatalf("expected installation React route, got %q", location)
794794
}
795795

796-
req := httptest.NewRequest(http.MethodGet, "/settings/channels/installations/ci_1/connections/cc_2", nil)
796+
req := httptest.NewRequest(http.MethodGet, "/settings/channels/installations/chinst_workspace_1/connections/chconn_secondary", nil)
797797
req.Header.Set("X-Spritz-User-Id", "user-1")
798798
rec := httptest.NewRecorder()
799799
gateway.routes().ServeHTTP(rec, req)
800800

801801
if rec.Code != http.StatusSeeOther {
802802
t.Fatalf("expected connection redirect, got %d: %s", rec.Code, rec.Body.String())
803803
}
804-
if location := rec.Header().Get("Location"); location != "https://gateway.example.test/settings/slack/channels/installations/ci_1/connections/cc_2" {
804+
if location := rec.Header().Get("Location"); location != "https://gateway.example.test/settings/slack/channels/installations/chinst_workspace_1/connections/chconn_secondary" {
805805
t.Fatalf("expected connection React route, got %q", location)
806806
}
807807
}
@@ -891,7 +891,7 @@ func TestChannelSettingsListDoesNotInventLegacyConnectionIDs(t *testing.T) {
891891
"status": "resolved",
892892
"installations": []map[string]any{
893893
{
894-
"id": "ci_1",
894+
"id": "chinst_workspace_1",
895895
"route": map[string]any{
896896
"principalId": "shared-slack-gateway",
897897
"provider": "slack",
@@ -942,7 +942,7 @@ func TestChannelSettingsUpdatePostsRoutePolicies(t *testing.T) {
942942
"status": "resolved",
943943
"installations": []map[string]any{
944944
{
945-
"id": "ci_1",
945+
"id": "chinst_workspace_1",
946946
"route": map[string]any{
947947
"principalId": "shared-slack-gateway",
948948
"provider": "slack",
@@ -952,24 +952,24 @@ func TestChannelSettingsUpdatePostsRoutePolicies(t *testing.T) {
952952
"state": "ready",
953953
"connections": []map[string]any{
954954
{
955-
"id": "cc_1",
955+
"id": "chconn_default",
956956
"isDefault": true,
957957
"state": "ready",
958958
"routes": []map[string]any{
959959
{
960-
"id": "cr_1",
960+
"id": "chroute_existing",
961961
"externalChannelId": "C_existing",
962962
"requireMention": true,
963963
"enabled": true,
964964
},
965965
},
966966
},
967967
{
968-
"id": "cc_2",
968+
"id": "chconn_secondary",
969969
"state": "ready",
970970
"routes": []map[string]any{
971971
{
972-
"id": "cr_2",
972+
"id": "chroute_channel_2",
973973
"externalChannelId": "C_channel_2",
974974
"requireMention": true,
975975
"enabled": true,
@@ -1010,7 +1010,7 @@ func TestChannelSettingsUpdatePostsRoutePolicies(t *testing.T) {
10101010

10111011
req := httptest.NewRequest(
10121012
http.MethodPost,
1013-
"/settings/channels/installations/ci_1/connections/cc_1",
1013+
"/settings/channels/installations/chinst_workspace_1/connections/chconn_default",
10141014
strings.NewReader("action=upsert&externalChannelId=C_new"),
10151015
)
10161016
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -1024,7 +1024,7 @@ func TestChannelSettingsUpdatePostsRoutePolicies(t *testing.T) {
10241024
if updatePayload["callerAuthId"] != "user-1" {
10251025
t.Fatalf("expected caller auth id, got %#v", updatePayload["callerAuthId"])
10261026
}
1027-
if updatePayload["installationId"] != "ci_1" || updatePayload["connectionId"] != "cc_1" {
1027+
if updatePayload["installationId"] != "chinst_workspace_1" || updatePayload["connectionId"] != "chconn_default" {
10281028
t.Fatalf("expected installation and connection ids, got %#v", updatePayload)
10291029
}
10301030
policies, ok := updatePayload["channelPolicies"].([]any)

ui/src/lib/slack-management.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,15 @@ describe('slack gateway path helpers', () => {
2828
expect(slackGatewayPath('/api/settings/channels')).toBe('/api/settings/channels');
2929
expect(slackGatewayPath('/slack/install')).toBe('/slack/install');
3030
});
31+
32+
it('preserves an absolute Slack gateway base URL', async () => {
33+
const { slackGatewayBasePath, slackGatewayPath } = await loadSlackManagement({
34+
slackGatewayBasePath: 'https://gateway.example.test/slack-gateway/',
35+
});
36+
37+
expect(slackGatewayBasePath()).toBe('https://gateway.example.test/slack-gateway');
38+
expect(slackGatewayPath('/slack/workspaces')).toBe(
39+
'https://gateway.example.test/slack-gateway/slack/workspaces',
40+
);
41+
});
3142
});

ui/src/lib/slack-management.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface SlackWorkspaceTestResult {
8585
export function slackGatewayBasePath(): string {
8686
const configured = String(config.slackGatewayBasePath || '').trim();
8787
if (/^\/+$/.test(configured)) return '';
88+
if (/^https?:\/\//i.test(configured)) return configured.replace(/\/+$/g, '');
8889
const normalized = (configured || '/slack-gateway').replace(/\/+$/g, '');
8990
if (!normalized) return '/slack-gateway';
9091
return normalized.startsWith('/') ? normalized : `/${normalized}`;

ui/src/pages/chat.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,13 @@ describe('ChatPage draft persistence', () => {
509509
});
510510
});
511511

512+
it('routes the settings entrypoint through the Slack gateway', async () => {
513+
await renderChat('/c/covo/conv-1');
514+
515+
const settingsLink = screen.getByLabelText('Open settings') as HTMLAnchorElement;
516+
expect(settingsLink.getAttribute('href')).toBe('/slack-gateway/slack/workspaces');
517+
});
518+
512519
it('retries ACP connect-ticket failures automatically', async () => {
513520
setAuthToken('external-ui-token');
514521
let ticketAttempts = 0;

ui/src/pages/chat.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback, useRef } from 'react';
2-
import { Link, useParams, useNavigate } from 'react-router-dom';
2+
import { useParams, useNavigate } from 'react-router-dom';
33
import { toast } from 'sonner';
44
import { MenuIcon, RotateCwIcon, ExternalLinkIcon, SettingsIcon } from 'lucide-react';
55
import { request } from '@/lib/api';
@@ -18,6 +18,7 @@ import {
1818
getConversationAgentName,
1919
} from '@/lib/spritz-profile';
2020
import { chatConversationPath } from '@/lib/urls';
21+
import { slackGatewayPath } from '@/lib/slack-management';
2122
import { AgentAvatar } from '@/components/agent-avatar';
2223
import { useNotice } from '@/components/notice-banner';
2324
import { Sidebar } from '@/components/acp/sidebar';
@@ -573,8 +574,8 @@ export function ChatPage() {
573574
<Tooltip>
574575
<TooltipTrigger
575576
render={
576-
<Link
577-
to="/settings/slack/workspaces"
577+
<a
578+
href={slackGatewayPath('/slack/workspaces')}
578579
aria-label="Open settings"
579580
className="inline-flex size-9 items-center justify-center rounded-[var(--radius-md)] border border-border bg-background text-foreground transition-colors hover:bg-muted"
580581
/>

0 commit comments

Comments
 (0)