Skip to content

Commit b750b40

Browse files
author
Faxbot Agent
committed
feat(ui): enhance Admin Console with new features and deep linking
- Implemented anchor mapping for precise help links in the Admin Console, allowing users to access specific documentation sections directly. - Added new navigation cases in the App component to support tools and settings tabs, improving user experience. - Introduced a new admin endpoint to purge inbound faxes by provider SID, enhancing local development capabilities. - Updated the Dashboard component to navigate to specific settings sections, streamlining access to configuration options. - Enhanced the Diagnostics component with third-party links for Sinch integration, providing users with direct access to relevant documentation. These changes improve the usability and functionality of the Admin Console, making it easier for users to navigate and access necessary resources.
1 parent 862c4e1 commit b750b40

File tree

12 files changed

+381
-30
lines changed

12 files changed

+381
-30
lines changed

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ Quick references
206206
- App shell: `api/admin_ui/src/App.tsx`:1
207207
- Electron: `api/admin_ui/electron/main.js`:1, `api/admin_ui/electron/preload.js`:1
208208

209+
Anchor mapping for precise help links (Docs → UI)
210+
- The Admin Console Diagnostics and Settings consume JSON anchor maps from the docs site to deep-link directly to exact sentences.
211+
- Location (docs site): `${docsBase}/anchors/<provider>.json` (e.g., anchors/sinch.json).
212+
- Schema: a flat object `{ "topic-key": "https://.../page#anchor-id" }`.
213+
- Examples of topic keys we use today:
214+
- Sinch: `sinch-build-access-keys-location`, `sinch-oauth-client-credentials-flow`, `sinch-regional-base-url`, `sinch-inbound-webhook-url`, `sinch-inbound-basic-auth`, `sinch-troubleshoot-auth-fail`, `sinch-troubleshoot-inbound-fail`, `sinch-register-webhook-limitations`.
215+
- If an anchor mapping is present, the UI will prefer it over generic links. Keep these JSON files up to date as docs evolve to avoid stale or vague tips.
216+
209217
### How To Add A New Screen
210218

211219
1) Create the component

api/admin_ui/src/App.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,22 @@ function AppContent() {
161161
setTabValue(5);
162162
setToolsTab(2);
163163
break;
164+
case 'tools/terminal':
165+
setTabValue(5);
166+
setToolsTab(0);
167+
break;
168+
case 'tools/plugins':
169+
setTabValue(5);
170+
setToolsTab(3); // Plugins entry only present when enabled
171+
break;
172+
case 'tools/scripts':
173+
setTabValue(5);
174+
setToolsTab(adminConfig?.v3_plugins?.enabled ? 4 : 3);
175+
break;
176+
case 'tools/tunnels':
177+
setTabValue(5);
178+
setToolsTab(adminConfig?.v3_plugins?.enabled ? 5 : 4);
179+
break;
164180
case 'settings/setup':
165181
setTabValue(4);
166182
setSettingsTab(0);
@@ -169,6 +185,70 @@ function AppContent() {
169185
setTabValue(4);
170186
setSettingsTab(1);
171187
break;
188+
case 'settings/security':
189+
setTabValue(4);
190+
setSettingsTab(1);
191+
setTimeout(() => {
192+
const el = document.querySelector('#settings-security');
193+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
194+
}, 150);
195+
break;
196+
case 'settings/phaxio':
197+
setTabValue(4);
198+
setSettingsTab(1);
199+
setTimeout(() => {
200+
const el = document.querySelector('#settings-phaxio');
201+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
202+
}, 150);
203+
break;
204+
case 'settings/sinch':
205+
setTabValue(4);
206+
setSettingsTab(1);
207+
setTimeout(() => {
208+
const el = document.querySelector('#settings-sinch');
209+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
210+
}, 150);
211+
break;
212+
case 'settings/sip':
213+
setTabValue(4);
214+
setSettingsTab(1);
215+
setTimeout(() => {
216+
const el = document.querySelector('#settings-sip');
217+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
218+
}, 150);
219+
break;
220+
case 'settings/inbound':
221+
setTabValue(4);
222+
setSettingsTab(1);
223+
setTimeout(() => {
224+
const el = document.querySelector('#settings-inbound');
225+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
226+
}, 150);
227+
break;
228+
case 'settings/storage':
229+
setTabValue(4);
230+
setSettingsTab(1);
231+
setTimeout(() => {
232+
const el = document.querySelector('#settings-storage');
233+
(el as any)?.scrollIntoView?.({ behavior: 'smooth' });
234+
}, 150);
235+
break;
236+
case 'settings/mcp':
237+
setTabValue(4);
238+
setSettingsTab(3);
239+
break;
240+
case 'jobs':
241+
setTabValue(2);
242+
break;
243+
case 'send':
244+
setTabValue(1);
245+
break;
246+
case 'inbound':
247+
setTabValue(3);
248+
break;
249+
case 'dashboard':
250+
setTabValue(0);
251+
break;
172252
default:
173253
setTabValue(0);
174254
}

api/admin_ui/src/api/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ export class AdminAPIClient {
227227
return res.json();
228228
}
229229

230+
async purgeInboundBySid(providerSid: string): Promise<{ ok: boolean; deleted_faxes: number; deleted_events: number }>{
231+
const res = await this.fetch('/admin/inbound/purge-by-sid', {
232+
method: 'DELETE',
233+
body: JSON.stringify({ provider_sid: providerSid })
234+
});
235+
return res.json();
236+
}
237+
230238
async simulateInbound(opts: { backend?: string; fr?: string; to?: string; pages?: number; status?: string } = {}): Promise<{ id: string; status: string }> {
231239
const res = await this.fetch('/admin/inbound/simulate', {
232240
method: 'POST',

api/admin_ui/src/components/Dashboard.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ function Dashboard({ client, onNavigate }: DashboardProps) {
255255
},
256256
transition: 'all 0.2s ease-in-out',
257257
}}
258-
onClick={() => onNavigate?.(4)} // Navigate to Settings tab (index 4)
258+
onClick={() => onNavigate?.('settings/security')}
259259
>
260260
<CardContent sx={{ pb: { xs: 1, sm: 2 } }}>
261261
<Typography variant="h6" component="h2" gutterBottom sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
@@ -282,7 +282,7 @@ function Dashboard({ client, onNavigate }: DashboardProps) {
282282

283283
{/* Config Overview */}
284284
<Grid item xs={12} md={6}>
285-
<Card>
285+
<Card sx={{ cursor: 'pointer' }} onClick={() => onNavigate?.('settings/settings')}>
286286
<CardContent>
287287
<Typography variant="h6" gutterBottom>Config Overview</Typography>
288288
<Grid container spacing={1}>
@@ -303,7 +303,7 @@ function Dashboard({ client, onNavigate }: DashboardProps) {
303303

304304
{/* MCP Overview */}
305305
<Grid item xs={12} md={6}>
306-
<Card>
306+
<Card sx={{ cursor: 'pointer' }} onClick={() => onNavigate?.('settings/mcp')}>
307307
<CardContent>
308308
<Typography variant="h6" gutterBottom>MCP Overview</Typography>
309309
<Grid container spacing={1} alignItems="center">
@@ -339,7 +339,7 @@ function Dashboard({ client, onNavigate }: DashboardProps) {
339339
{/* Plugins (feature-gated) */}
340340
{cfg?.v3_plugins?.enabled && (
341341
<Grid item xs={12} md={6}>
342-
<Card>
342+
<Card sx={{ cursor: 'pointer' }} onClick={() => onNavigate?.('tools/plugins')}>
343343
<CardContent>
344344
<Typography variant="h6" gutterBottom>Plugins</Typography>
345345
<Typography variant="body2" color="text.secondary">
@@ -355,7 +355,7 @@ function Dashboard({ client, onNavigate }: DashboardProps) {
355355

356356
{/* SDK & Quickstart */}
357357
<Grid item xs={12} md={cfg?.v3_plugins?.enabled ? 6 : 12}>
358-
<Card>
358+
<Card sx={{ cursor: 'pointer' }} onClick={() => onNavigate?.('send')}>
359359
<CardContent>
360360
<Typography variant="h6" gutterBottom>SDK & Quickstart</Typography>
361361
<Typography variant="body2">Base URL: {window.location.origin}</Typography>
@@ -376,7 +376,7 @@ PY`}</Box>
376376

377377
{/* Last Updated */}
378378
<Grid item xs={12}>
379-
<Card>
379+
<Card sx={{ cursor: 'pointer' }} onClick={() => onNavigate?.('tools/diagnostics')}>
380380
<CardContent>
381381
<Typography variant="body2" color="text.secondary">
382382
Last updated: {new Date(health.timestamp).toLocaleString()}

api/admin_ui/src/components/Diagnostics.tsx

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,54 @@ function Diagnostics({ client, onNavigate, docsBase }: DiagnosticsProps) {
6666
const [testJobId, setTestJobId] = useState<string | null>(null);
6767
const [testStatus, setTestStatus] = useState<string | null>(null);
6868
const [expandedSections, setExpandedSections] = useState<string[]>(['summary']);
69+
const [anchors, setAnchors] = useState<Record<string, string>>({});
6970

7071
const theme = useTheme();
7172
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
7273
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
7374
const { active, registry } = useTraits();
75+
const thirdParty: Record<string, string> = {
76+
'sinch-build-access-keys-location': 'https://dashboard.sinch.com/settings/access-keys',
77+
'sinch-oauth-client-credentials-flow': 'https://developers.sinch.com/docs/fax/api-reference/authentication/oauth/',
78+
'sinch-regional-base-url': 'https://developers.sinch.com/docs/fax/api-reference/#global-url',
79+
'sinch-inbound-webhook-url': 'https://developers.sinch.com/docs/fax/api-reference/fax/tag/Notifications/#incoming-fax-event-webhook',
80+
'sinch-inbound-basic-auth': 'https://developers.sinch.com/docs/fax/api-reference/fax/tag/Notifications/#incoming-fax-event-webhook',
81+
'sinch-register-webhook-limitations': 'https://developers.sinch.com/docs/fax/api-reference/fax/tag/Services/#create-a-service',
82+
'sinch-troubleshoot-auth-fail': 'https://developers.sinch.com/docs/fax/api-reference/fax/tag/Error-Messages/#http-error-codes',
83+
'sinch-troubleshoot-inbound-fail': 'https://developers.sinch.com/docs/fax/api-reference/fax/tag/Services/#create-a-service',
84+
'enforce-https-phi': 'https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164/subpart-C/section-164.312',
85+
'require-api-key-production': 'https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html#https',
86+
'audit-logging-hipaa': 'https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164/subpart-C/section-164.312',
87+
'phaxio-webhook-hmac': 'https://www.phaxio.com/docs/security/callbacks',
88+
'phaxio-status-callback-url': 'https://www.phaxio.com/docs/api/v1/send/sendCallback',
89+
};
90+
91+
const hrefFor = (topic: string): string | undefined => (anchors[topic] || thirdParty[topic]);
92+
93+
// Load anchor mapping from docs site for precise deep links
94+
useEffect(() => {
95+
const loadAnchors = async () => {
96+
try {
97+
const base = docsBase || 'https://dmontgomery40.github.io/Faxbot';
98+
const topics: string[] = [ 'security', 'diagnostics', 'inbound', 'storage', 'plugins', 'mcp' ];
99+
const provs = [active?.outbound, active?.inbound].filter(Boolean) as string[];
100+
for (const p of provs) { if (!topics.includes(p)) topics.push(p); }
101+
topics.push('all');
102+
for (const t of Array.from(new Set(topics))) {
103+
try {
104+
const res = await fetch(`${base}/anchors/${t}.json`, { cache: 'no-store' });
105+
if (res.ok) {
106+
const js = await res.json();
107+
setAnchors(prev => ({ ...prev, ...js }));
108+
}
109+
} catch { /* ignore per-scope failure */ }
110+
}
111+
} catch {
112+
/* ignore */
113+
}
114+
};
115+
loadAnchors();
116+
}, [docsBase, active?.outbound, active?.inbound]);
74117

75118
const runDiagnostics = async () => {
76119
try {
@@ -203,9 +246,9 @@ function Diagnostics({ client, onNavigate, docsBase }: DiagnosticsProps) {
203246
if (t.includes('phaxio')) {
204247
docs.push({ text: 'Phaxio Setup Guide', href: `${docsBase || 'https://dmontgomery40.github.io/Faxbot'}/backends/phaxio-setup.html` });
205248
docs.push({ text: 'Phaxio Console', href: 'https://console.phaxio.com' });
206-
if (key === 'public_url_https' || key === 'callback_url_set') {
207-
docs.push({ text: 'Webhook security requires HTTPS for PHI transmission.' });
208-
}
249+
const add = (topic: string, text: string) => { const href = anchors[topic] || thirdParty[topic]; if (href) docs.push({ text, href }); };
250+
add('phaxio-webhook-hmac', 'Verify Phaxio inbound HMAC signatures');
251+
add('phaxio-status-callback-url', 'Set status callback URL (HTTPS required)');
209252
}
210253

211254
if (t.includes('sip')) {
@@ -217,16 +260,26 @@ function Diagnostics({ client, onNavigate, docsBase }: DiagnosticsProps) {
217260

218261
if (t.includes('security')) {
219262
docs.push({ text: 'Security Guide', href: `${docsBase || 'https://dmontgomery40.github.io/Faxbot'}/security/` });
220-
if (key === 'enforce_https') {
221-
docs.push({ text: 'HIPAA requires encryption in transit. Enable ENFORCE_PUBLIC_HTTPS=true.' });
222-
}
263+
const addSec = (topic: string, text: string) => { const href = anchors[topic] || thirdParty[topic]; if (href) docs.push({ text, href }); };
264+
addSec('enforce-https-phi', 'Enforce HTTPS for PHI (ENFORCE_PUBLIC_HTTPS)');
265+
addSec('require-api-key-production', 'Require API keys (REQUIRE_API_KEY)');
266+
addSec('audit-logging-hipaa', 'Enable audit logging');
223267
}
224268

225269
if (t.includes('sinch')) {
226270
docs.push({ text: 'Faxbot: Sinch Setup', href: `${docsBase || 'https://dmontgomery40.github.io/Faxbot'}/backends/sinch-setup.html` });
227271
docs.push({ text: 'Sinch Fax API', href: 'https://developers.sinch.com/docs/fax/api-reference/' });
228272
docs.push({ text: 'OAuth 2.0 for Fax API', href: 'https://developers.sinch.com/docs/fax/api-reference/authentication/oauth/' });
229273
docs.push({ text: 'Sinch Customer Dashboard (Access Keys – Build)', href: 'https://dashboard.sinch.com/settings/access-keys' });
274+
const add = (topic: string, text: string) => { const href = anchors[topic] || thirdParty[topic]; if (href) docs.push({ text, href }); };
275+
add('sinch-build-access-keys-location', 'Where to find Sinch Fax access keys (Build)');
276+
add('sinch-oauth-client-credentials-flow', 'How Faxbot mints OAuth2 access tokens');
277+
add('sinch-regional-base-url', 'Regional base URL (SINCH_BASE_URL)');
278+
add('sinch-inbound-webhook-url', 'Set the inbound webhook URL');
279+
add('sinch-inbound-basic-auth', 'Enforce Basic auth for inbound webhooks');
280+
add('sinch-troubleshoot-auth-fail', 'Troubleshooting auth failures in Diagnostics');
281+
add('sinch-troubleshoot-inbound-fail', 'Troubleshooting inbound failures');
282+
add('sinch-register-webhook-limitations', 'Limitations of auto “Register with Sinch”');
230283
}
231284

232285
return docs;

api/admin_ui/src/components/ScriptsTests.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
157157
const [actions, setActions] = useState<Array<{ id: string; label: string }>>([]);
158158
const [actionOutput, setActionOutput] = useState<Record<string, string>>({});
159159
const [activeActionTab, setActiveActionTab] = useState<string>('');
160+
const [purgeSid, setPurgeSid] = useState<string>('');
161+
const [purgeResult, setPurgeResult] = useState<string>('');
162+
const [purging, setPurging] = useState<boolean>(false);
160163

161164
const theme = useTheme();
162165

@@ -249,6 +252,21 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
249252
} finally { setBusyInfo(false); }
250253
};
251254

255+
const runPurgeInbound = async () => {
256+
setError(''); setPurging(true); setPurgeResult('');
257+
try {
258+
if (!purgeSid.trim()) throw new Error('Enter a provider_sid');
259+
const res = await (client as any).purgeInboundBySid?.(purgeSid.trim());
260+
if (res && res.ok) {
261+
setPurgeResult(`Deleted faxes=${res.deleted_faxes}, events=${res.deleted_events}`);
262+
} else {
263+
setPurgeResult('No items deleted');
264+
}
265+
} catch (e: any) {
266+
setError(e?.message || 'Purge failed');
267+
} finally { setPurging(false); }
268+
};
269+
252270
const generateSecret = () => {
253271
try {
254272
if (window.crypto && (window.crypto as any).getRandomValues) {
@@ -529,6 +547,12 @@ const ScriptsTests: React.FC<Props> = ({ client, docsBase }) => {
529547
</Button>
530548
</Stack>
531549
<ConsoleBox lines={infoLines} loading={busyInfo} />
550+
<Typography variant="subtitle2" sx={{ mt: 1 }}>Database Helpers</Typography>
551+
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} alignItems={{ xs: 'stretch', sm: 'center' }}>
552+
<ResponsiveTextField label="provider_sid" value={purgeSid} onChange={setPurgeSid} placeholder="fax_123 or provider-specific id" />
553+
<Button variant="contained" onClick={runPurgeInbound} disabled={purging} sx={{ borderRadius: 2 }}>{purging ? 'Purging…' : 'Purge Inbound by SID'}</Button>
554+
{purgeResult && <Chip size="small" color="info" variant="outlined" label={purgeResult} />}
555+
</Stack>
532556
</Stack>
533557
</ResponsiveFormSection>
534558
</Grid>

api/admin_ui/src/components/Settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ function Settings({ client }: SettingsProps) {
354354
</ResponsiveFormSection>
355355

356356
{/* Security Settings */}
357+
<Box id="settings-security" />
357358
<ResponsiveFormSection
358359
title="Security Settings"
359360
subtitle="Configure authentication and HIPAA compliance"
@@ -451,6 +452,7 @@ function Settings({ client }: SettingsProps) {
451452

452453
{/* Backend-Specific Configuration */}
453454
{active?.outbound === 'phaxio' && (
455+
<Box id="settings-phaxio" />
454456
<ResponsiveSettingSection
455457
title="PHAXIO Configuration"
456458
subtitle="Configure your Phaxio API credentials and settings"
@@ -503,6 +505,7 @@ function Settings({ client }: SettingsProps) {
503505
)}
504506

505507
{active?.outbound === 'sinch' && (
508+
<Box id="settings-sinch" />
506509
<ResponsiveSettingSection
507510
title="Sinch Fax API v3 Configuration"
508511
subtitle="Outbound API uses OAuth 2.0 (Bearer). Inbound webhooks are not provider‑signed; you may enforce Basic auth on your endpoint."
@@ -648,6 +651,7 @@ function Settings({ client }: SettingsProps) {
648651
)}
649652

650653
{active?.outbound === 'sip' && (
654+
<Box id="settings-sip" />
651655
<ResponsiveSettingSection
652656
title="SIP / Asterisk Configuration"
653657
subtitle="Configure your Asterisk AMI connection settings"
@@ -761,6 +765,7 @@ function Settings({ client }: SettingsProps) {
761765
</ResponsiveFormSection>
762766

763767
{/* Inbound Receiving */}
768+
<Box id="settings-inbound" />
764769
<ResponsiveFormSection
765770
title="Inbound Receiving"
766771
subtitle="Configure inbound fax receiving and storage settings"
@@ -1013,6 +1018,7 @@ function Settings({ client }: SettingsProps) {
10131018
</Card>
10141019
</Grid>
10151020
)}
1021+
<Box id="settings-storage" />
10161022
<ResponsiveFormSection
10171023
title="Storage Configuration"
10181024
subtitle="Configure file storage backend and S3 settings"

0 commit comments

Comments
 (0)