({});
+ // TRA-922: every publish_topic must start with {org_slug}/. Pre-fill the
+ // create form with that prefix so operators only type the device-specific tail.
+ const orgSlug = useOrgStore((s) => s.currentOrg?.identifier ?? '');
+
useEffect(() => {
if (mode === 'edit' && device) {
setFormData({
@@ -80,9 +85,9 @@ export function ScanDeviceForm({
is_active: device.is_active,
});
} else if (mode === 'create') {
- setFormData(EMPTY_FORM);
+ setFormData({ ...EMPTY_FORM, publish_topic: orgSlug ? `${orgSlug}/` : '' });
}
- }, [mode, device]);
+ }, [mode, device, orgSlug]);
const validateForm = (): boolean => {
const errors: FieldErrors = {};
@@ -233,13 +238,18 @@ export function ScanDeviceForm({
onChange={(e) => handleChange('publish_topic', e.target.value)}
disabled={loading}
className={inputClass(!!fieldErrors.publish_topic)}
- placeholder="e.g., trakrf.id/dock-reader-1/reads"
+ placeholder={orgSlug ? `e.g., ${orgSlug}/dock-reader-1/reads` : 'e.g., your-org/dock-reader-1/reads'}
/>
{fieldErrors.publish_topic && (
{fieldErrors.publish_topic}
)}
The MQTT topic this device publishes reads on — the routing key that ties wire traffic to this reader.
+ {orgSlug && (
+ <>
+ {' '}Must start with {orgSlug}/.
+ >
+ )}
diff --git a/frontend/src/components/scandevices/ScanDeviceFormModal.test.tsx b/frontend/src/components/scandevices/ScanDeviceFormModal.test.tsx
index 869826ee..924e2e63 100644
--- a/frontend/src/components/scandevices/ScanDeviceFormModal.test.tsx
+++ b/frontend/src/components/scandevices/ScanDeviceFormModal.test.tsx
@@ -162,8 +162,8 @@ describe('ScanDeviceFormModal', () => {
);
- // publish_topic is trakrf.id/dock_reader_1/reads → key dock_reader_1.
- expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:dock_reader_1');
+ // publish_topic is the reader key verbatim (TRA-922: no parsing).
+ expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:trakrf.id/dock_reader_1/reads');
});
it('does not show the commissioning sections in create mode', () => {
@@ -191,7 +191,7 @@ describe('ScanDeviceFormModal', () => {
).toBeInTheDocument();
// …along with the scoped commissioning sections…
expect(screen.getByTestId('reader-points')).toHaveTextContent('points:csl_cs463');
- expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:dock_reader_1');
+ expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:trakrf.id/dock_reader_1/reads');
// …but the modal chrome (backdrop close button + title bar) is gone.
expect(screen.queryByLabelText('Close modal')).not.toBeInTheDocument();
expect(
@@ -223,7 +223,7 @@ describe('ScanDeviceFormModal', () => {
expect(
screen.getByRole('button', { name: /Update Scan Device/i })
).toBeInTheDocument();
- expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:dock_reader_1');
+ expect(screen.getByTestId('scoped-feed')).toHaveTextContent('feed:trakrf.id/dock_reader_1/reads');
expect(screen.queryByLabelText('Close modal')).not.toBeInTheDocument();
});
});
diff --git a/frontend/src/lib/scandevices/deviceProfile.test.ts b/frontend/src/lib/scandevices/deviceProfile.test.ts
index 25f37344..fb0958cd 100644
--- a/frontend/src/lib/scandevices/deviceProfile.test.ts
+++ b/frontend/src/lib/scandevices/deviceProfile.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { deviceProfile, readerKeyFromTopic, readerKeyForDevice } from './deviceProfile';
+import { deviceProfile, readerKeyForDevice } from './deviceProfile';
import type { ScanDevice } from '@/types/scandevices';
function device(overrides: Partial): ScanDevice {
@@ -46,21 +46,13 @@ describe('deviceProfile', () => {
});
});
-describe('readerKeyFromTopic', () => {
- it('extracts the {key} segment from a standard reads topic', () => {
- expect(readerKeyFromTopic('trakrf.id/dock-7/reads')).toBe('dock-7');
- });
-
- it('falls back to the full topic for non-matching strings', () => {
- expect(readerKeyFromTopic('weird/topic')).toBe('weird/topic');
- });
-});
-
describe('readerKeyForDevice', () => {
- it('derives the key from publish_topic when present', () => {
+ // TRA-922: the reader key is the publish_topic verbatim (direct topic→device
+ // match; nothing is parsed out of it), matching how the backend keys reads.
+ it('uses the publish_topic verbatim as the key', () => {
expect(
- readerKeyForDevice(device({ publish_topic: 'trakrf.id/custom-key/reads' }))
- ).toBe('custom-key');
+ readerKeyForDevice(device({ publish_topic: 'organized-chaos/custom-key/reads' }))
+ ).toBe('organized-chaos/custom-key/reads');
});
// TRA-956: external_key is gone — a device with no publish_topic has no
diff --git a/frontend/src/lib/scandevices/deviceProfile.ts b/frontend/src/lib/scandevices/deviceProfile.ts
index f876ec3b..40cfa03b 100644
--- a/frontend/src/lib/scandevices/deviceProfile.ts
+++ b/frontend/src/lib/scandevices/deviceProfile.ts
@@ -29,26 +29,14 @@ export function deviceProfile(device: Pick): D
return 'single_point';
}
-const TOPIC_RE = /^trakrf\.id\/([^/]+)\/reads$/;
-
-/**
- * Extract the reader key from a `trakrf.id/{key}/reads` topic, mirroring the
- * backend's readerKeyFromTopic (broadcaster.go). Non-matching topics fall back
- * to the full string so the key is never empty.
- */
-export function readerKeyFromTopic(topic: string): string {
- const m = TOPIC_RE.exec(topic);
- return m ? m[1] : topic;
-}
-
/**
- * The reader key the live feed tags this device's reads with. The backend
- * derives readerKey from the topic a device publishes on, so we derive the same
- * key from publish_topic. A device with no publish_topic has no live-feed key.
+ * The reader key the live feed tags this device's reads with. TRA-922: routing
+ * is a direct topic→device match (no parsing), so the reader key is simply the
+ * device's publish_topic used verbatim — the same string the backend keys reads
+ * by. A device with no publish_topic has no live-feed key.
*/
export function readerKeyForDevice(
device: Pick
): string {
- const topic = device.publish_topic?.trim();
- return topic ? readerKeyFromTopic(topic) : '';
+ return device.publish_topic?.trim() ?? '';
}
diff --git a/frontend/src/types/org/index.ts b/frontend/src/types/org/index.ts
index f10274ea..6a5bcf21 100644
--- a/frontend/src/types/org/index.ts
+++ b/frontend/src/types/org/index.ts
@@ -20,6 +20,8 @@ export interface UserOrg {
export interface UserOrgWithRole {
id: number;
name: string;
+ /** Globally-unique URL-safe slug; used to pre-fill the {org_slug}/ publish_topic prefix (TRA-922). */
+ identifier: string;
role: OrgRole;
}